#728 - Polishing.

Adapt to API changes in master branch. Fixed formatting.
This commit is contained in:
Oliver Drotbohm
2019-02-15 13:13:22 +01:00
parent 3f0dcc7cb4
commit 68c605c7ff
29 changed files with 1748 additions and 1030 deletions

View File

@@ -13,13 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
package org.springframework.hateoas;
import static org.springframework.hateoas.TemplateVariable.VariableType.*;
import static org.springframework.hateoas.TemplateVariables.*;
import static org.springframework.hateoas.core.EncodingUtils.*;
import static org.springframework.web.util.UriComponents.UriTemplateVariables.*;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -30,13 +35,21 @@ import java.util.function.BiFunction;
import java.util.function.Function;
import org.springframework.core.MethodParameter;
import org.springframework.hateoas.TemplateVariable;
import org.springframework.hateoas.TemplateVariables;
import org.springframework.hateoas.core.AnnotatedParametersParameterAccessor.BoundMethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.hateoas.core.AnnotationAttribute;
import org.springframework.hateoas.core.AnnotationMappingDiscoverer;
import org.springframework.hateoas.core.LastInvocationAware;
import org.springframework.hateoas.core.MappingDiscoverer;
import org.springframework.hateoas.core.MethodInvocation;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -47,52 +60,45 @@ import org.springframework.web.util.UriTemplate;
/**
* Utility for taking a a method invocation and extracting a {@link ControllerLinkBuilder}.
*
*
* @author Greg Turnquist
*/
public class WebHandler {
private static final MappingDiscoverer DISCOVERER = new AnnotationMappingDiscoverer(RequestMapping.class);
private static final AnnotatedParametersParameterAccessor PATH_VARIABLE_ACCESSOR = new AnnotatedParametersParameterAccessor(
new AnnotationAttribute(PathVariable.class));
new AnnotationAttribute(PathVariable.class));
private static final AnnotatedParametersParameterAccessor REQUEST_PARAM_ACCESSOR = new RequestParamParameterAccessor();
public static ControllerLinkBuilder linkTo(Object invocationValue,
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder,
BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler) {
Function<String, UriComponentsBuilder> mappingToUriComponentsBuilder,
BiFunction<UriComponentsBuilder, MethodInvocation, UriComponentsBuilder> additionalUriHandler) {
Assert.isInstanceOf(LastInvocationAware.class, invocationValue);
LastInvocationAware invocations = (LastInvocationAware) invocationValue;
MethodInvocation invocation = invocations.getLastInvocation();
String mapping = DISCOVERER.getMapping(invocation.getTargetType(), invocation.getMethod());
UriComponentsBuilder builder = mappingToUriComponentsBuilder.apply(mapping);
UriTemplate template;
if (mapping == null) {
template = new UriTemplate("/");
} else {
template = new UriTemplate(mapping);
}
UriTemplate template = new UriTemplate(mapping == null ? "/" : mapping);
Map<String, Object> values = new HashMap<>();
Iterator<String> names = template.getVariableNames().iterator();
Iterator<Object> classMappingParameters = invocations.getObjectParameters();
while (classMappingParameters.hasNext()) {
values.put(names.next(), encodePath(classMappingParameters.next()));
}
for (BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : PATH_VARIABLE_ACCESSOR.getBoundParameters(invocation)) {
values.put(parameter.getVariableName(), encodePath(parameter.asString()));
}
List<String> optionalEmptyParameters = new ArrayList<>();
for (BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
for (AnnotatedParametersParameterAccessor.BoundMethodParameter parameter : REQUEST_PARAM_ACCESSOR.getBoundParameters(invocation)) {
bindRequestParameters(builder, parameter);
@@ -112,12 +118,9 @@ public class WebHandler {
}
}
UriComponents components;
if (additionalUriHandler == null) {
components = builder.buildAndExpand(values);
} else {
components = additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
}
UriComponents components = additionalUriHandler == null //
? builder.buildAndExpand(values)
: additionalUriHandler.apply(builder, invocation).buildAndExpand(values);
TemplateVariables variables = NONE;
@@ -125,22 +128,22 @@ public class WebHandler {
boolean previousRequestParameter = components.getQueryParams().isEmpty() && variables.equals(NONE);
TemplateVariable variable = new TemplateVariable(parameter,
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
previousRequestParameter ? REQUEST_PARAM : REQUEST_PARAM_CONTINUED);
variables = variables.concat(variable);
}
return new ControllerLinkBuilder(components, variables, invocation);
}
/**
* Populates the given {@link UriComponentsBuilder} with request parameters found in the given
* {@link BoundMethodParameter}.
* {@link AnnotatedParametersParameterAccessor.BoundMethodParameter}.
*
* @param builder must not be {@literal null}.
* @param parameter must not be {@literal null}.
*/
@SuppressWarnings("unchecked")
private static void bindRequestParameters(UriComponentsBuilder builder, BoundMethodParameter parameter) {
private static void bindRequestParameters(UriComponentsBuilder builder, AnnotatedParametersParameterAccessor.BoundMethodParameter parameter) {
Object value = parameter.getValue();
String key = parameter.getVariableName();
@@ -198,7 +201,7 @@ public class WebHandler {
*/
@Override
protected BoundMethodParameter createParameter(final MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute) {
@@ -243,4 +246,162 @@ public class WebHandler {
return annotation.defaultValue().equals(ValueConstants.DEFAULT_NONE) ? SKIP_VALUE : null;
}
}
/**
* Value object to allow accessing {@link MethodInvocation} parameters with the configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
private static class AnnotatedParametersParameterAccessor {
private static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<>(
16, ConcurrentReferenceHashMap.ReferenceType.WEAK);
private final @NonNull AnnotationAttribute attribute;
/**
* Returns {@link BoundMethodParameter}s contained in the given {@link MethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(MethodInvocation invocation) {
Assert.notNull(invocation, "MethodInvocation must not be null!");
MethodParameters parameters = getOrCreateMethodParametersFor(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
Object value = arguments[parameter.getParameterIndex()];
Object verifiedValue = verifyParameterValue(parameter, value);
if (verifiedValue != null) {
result.add(createParameter(parameter, verifiedValue, attribute));
}
}
return result;
}
/**
* Create the {@link BoundMethodParameter} for the given {@link MethodParameter}, parameter value and
* {@link AnnotationAttribute}.
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute must not be {@literal null}.
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}
/**
* Callback to verify the parameter values given for a dummy invocation. Default implementation rejects
* {@literal null} values as they indicate an invalid dummy call.
*
* @param parameter will never be {@literal null}.
* @param value could be {@literal null}.
* @return the verified value.
*/
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
return value;
}
/**
* Returns the {@link MethodParameters} for the given {@link Method}.
*
* @param method
* @return
*/
private static MethodParameters getOrCreateMethodParametersFor(Method method) {
return METHOD_PARAMETERS_CACHE.computeIfAbsent(method, MethodParameters::new);
}
/**
* Represents a {@link MethodParameter} alongside the value it has been bound to.
*
* @author Oliver Gierke
*/
static class BoundMethodParameter {
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDescriptor;
/**
* Creates a new {@link BoundMethodParameter}
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute can be {@literal null}.
*/
public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
Assert.notNull(parameter, "MethodParameter must not be null!");
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, parameter.isOptional() ? 1 : 0);
}
/**
* Returns the name of the {@link UriTemplate} variable to be bound. The name will be derived from the configured
* {@link AnnotationAttribute} or the {@link MethodParameter} name as fallback.
*
* @return
*/
public String getVariableName() {
if (attribute == null) {
return parameter.getParameterName();
}
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
String annotationAttributeValue = attribute.getValueFrom(annotation);
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default conversion service setup.
*
* @return
*/
public String asString() {
return value == null //
? null //
: (String) CONVERSION_SERVICE.convert(value, parameterTypeDescriptor, STRING_DESCRIPTOR);
}
/**
* Returns whether the given parameter is a required one. Defaults to {@literal true}.
*
* @return
*/
public boolean isRequired() {
return true;
}
}
}
}

View File

@@ -95,15 +95,5 @@ public @interface EnableHypermediaSupport {
UBER;
private static Set<HypermediaType> HAL_BASED_MEDIATYPES = EnumSet.of(HAL, HAL_FORMS);
/**
* Is this {@literal HAL} or one of its derivatives?
*
* @param hypermediaType
* @return
*/
public static boolean isHalBasedMediaType(HypermediaType hypermediaType) {
return HAL_BASED_MEDIATYPES.contains(hypermediaType);
}
}
}

View File

@@ -18,7 +18,6 @@ package org.springframework.hateoas.config;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.HalConfiguration;
import org.springframework.hateoas.hal.Jackson2HalModule;
@@ -27,14 +26,13 @@ import org.springframework.hateoas.hal.forms.HalFormsConfiguration;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.uber.Jackson2UberModule;
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Utility class for create {@link ObjectMapper} instances of all hypermedia types.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -42,7 +40,7 @@ public final class HypermediaObjectMapperCreator {
/**
* Create a {@link org.springframework.hateoas.MediaTypes#HAL_JSON}-based {@link ObjectMapper}.
*
*
* @param objectMapper
* @param curieProvider
* @param relProvider
@@ -50,18 +48,15 @@ public final class HypermediaObjectMapperCreator {
* @param halConfiguration
* @return
*/
public static ObjectMapper createHalObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalConfiguration halConfiguration) {
public static ObjectMapper createHalObjectMapper(ObjectMapper objectMapper, CurieProvider curieProvider,
RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource, HalConfiguration halConfiguration) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, halConfiguration));
mapper.setHandlerInstantiator(
new HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource, halConfiguration));
return mapper;
}
@@ -76,19 +71,16 @@ public final class HypermediaObjectMapperCreator {
* @param halFormsConfiguration
* @return properly configured objectMapper
*/
public static ObjectMapper createHalFormsObjectMapper(ObjectMapper objectMapper,
CurieProvider curieProvider,
RelProvider relProvider,
MessageSourceAccessor linkRelationMessageSource,
HalFormsConfiguration halFormsConfiguration) {
public static ObjectMapper createHalFormsObjectMapper(ObjectMapper objectMapper, CurieProvider curieProvider,
RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource,
HalFormsConfiguration halFormsConfiguration) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(
relProvider, curieProvider, linkRelationMessageSource, true,
halFormsConfiguration));
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
true, halFormsConfiguration));
return mapper;
}
@@ -100,14 +92,12 @@ public final class HypermediaObjectMapperCreator {
* @param linkRelationMessageSource
* @return properly configured objectMapper
*/
public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper,
MessageSourceAccessor linkRelationMessageSource) {
public static ObjectMapper createCollectionJsonObjectMapper(ObjectMapper objectMapper) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
mapper.setHandlerInstantiator(new CollectionJsonHandlerInstantiator(linkRelationMessageSource));
return mapper;
}
@@ -124,7 +114,6 @@ public final class HypermediaObjectMapperCreator {
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return mapper;
}

View File

@@ -79,11 +79,11 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
if (converters.stream().filter(MappingJackson2HttpMessageConverter.class::isInstance)
.filter(AbstractJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
@@ -91,36 +91,32 @@ public class HypermediaWebMvcConfigurer implements WebMvcConfigurer, BeanFactory
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_JSON, HAL_JSON_UTF8),
createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource,
this.halConfiguration)));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_FORMS_JSON),
createHalFormsObjectMapper(this.mapper, this.curieProvider, this.relProvider, linkRelationMessageSource,
this.halFormsConfiguration)));
}
converters.add(0,
new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Arrays.asList(HAL_JSON, HAL_JSON_UTF8), createHalObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halConfiguration)));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
converters.add(0,
new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(HAL_FORMS_JSON), createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration)));
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(COLLECTION_JSON),
createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource)));
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Arrays.asList(COLLECTION_JSON), createCollectionJsonObjectMapper(this.mapper)));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(UBER_JSON), createUberObjectMapper(this.mapper)));
converters.add(0, new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(UBER_JSON), createUberObjectMapper(this.mapper)));
}
}
}

View File

@@ -26,6 +26,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
@@ -44,7 +45,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* {@link WebFluxConfigurer} to register hypermedia-aware {@link org.springframework.core.codec.Encoder}s and
* {@link org.springframework.core.codec.Decoder}s that will render hypermedia for WebFlux controllers.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -62,7 +63,7 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
@@ -70,8 +71,8 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto
/**
* Configure custom HTTP message readers and writers or override built-in ones.
* <p>The configured readers and writers will be used for both annotated
* controllers and functional endpoints.
* <p>
* The configured readers and writers will be used for both annotated controllers and functional endpoints.
*
* @param configurer the configurer to use
*/
@@ -79,67 +80,59 @@ public class HypermediaWebFluxConfigurer implements WebFluxConfigurer, BeanFacto
public void configureHttpMessageCodecs(ServerCodecConfigurer configurer) {
if (configurer.getReaders().stream()
.flatMap(httpMessageReader -> httpMessageReader.getReadableMediaTypes().stream())
.anyMatch(mediaType -> mediaType == MediaTypes.HAL_JSON
|| mediaType == MediaTypes.HAL_JSON_UTF8
|| mediaType == MediaTypes.HAL_FORMS_JSON
|| mediaType == MediaTypes.COLLECTION_JSON
|| mediaType == MediaTypes.UBER_JSON)) {
.flatMap(httpMessageReader -> httpMessageReader.getReadableMediaTypes().stream())
.anyMatch(mediaType -> mediaType == MediaTypes.HAL_JSON || mediaType == MediaTypes.HAL_JSON_UTF8
|| mediaType == MediaTypes.HAL_FORMS_JSON || mediaType == MediaTypes.COLLECTION_JSON
|| mediaType == MediaTypes.UBER_JSON)) {
// Already configured for hypermedia!
return;
}
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
MessageSourceAccessor.class);
CodecConfigurer.CustomCodecs customCodecs = configurer.customCodecs();
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
customCodecs.encoder(
new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
customCodecs.decoder(
new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
}
customCodecs.encoder(
new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
customCodecs.decoder(
new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
customCodecs.encoder(
new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
}
customCodecs.encoder(
new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource);
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper);
customCodecs.encoder(
new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
customCodecs.encoder(new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
customCodecs.decoder(new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
}
if (this.hypermediaTypes.contains(HypermediaType.UBER)) {
ObjectMapper uberObjectMapper = createUberObjectMapper(this.mapper);
customCodecs.encoder(
new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON));
customCodecs.decoder(
new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
customCodecs.encoder(new Jackson2JsonEncoder(uberObjectMapper, MediaTypes.UBER_JSON));
customCodecs.decoder(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
}
customCodecs.encoder(CharSequenceEncoder.allMimeTypes());
customCodecs.decoder(StringDecoder.allMimeTypes());
configurer.registerDefaults(false);

View File

@@ -28,6 +28,7 @@ import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.core.codec.CharSequenceEncoder;
import org.springframework.core.codec.Decoder;
import org.springframework.core.codec.Encoder;
import org.springframework.core.codec.StringDecoder;
@@ -46,7 +47,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Assembles {@link ExchangeStrategies} needed to wire a {@link WebClient} with hypermedia support.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -64,7 +65,7 @@ public class WebClientConfigurer implements BeanFactoryAware {
private final Collection<HypermediaType> hypermediaTypes;
private BeanFactory beanFactory;
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
@@ -72,40 +73,38 @@ public class WebClientConfigurer implements BeanFactoryAware {
/**
* Return a set of {@link ExchangeStrategies} driven by registered {@link HypermediaType}s.
*
* @return a collection of {@link Encoder}s and {@link Decoder} assembled into a {@link ExchangeStrategies}.
*/
public ExchangeStrategies hypermediaExchangeStrategies() {
MessageSourceAccessor linkRelationMessageSource = this.beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
MessageSourceAccessor.class);
List<Encoder<?>> encoders = new ArrayList<>();
List<Decoder<?>> decoders = new ArrayList<>();
if (this.hypermediaTypes.stream().anyMatch(HypermediaType::isHalBasedMediaType)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL)) {
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
ObjectMapper halObjectMapper = createHalObjectMapper(this.mapper, this.curieProvider, this.relProvider,
linkRelationMessageSource, this.halConfiguration);
encoders.add(new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
decoders.add(new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
}
encoders.add(new Jackson2JsonEncoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
decoders.add(new Jackson2JsonDecoder(halObjectMapper, MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8));
}
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
if (this.hypermediaTypes.contains(HypermediaType.HAL_FORMS)) {
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
ObjectMapper halFormsObjectMapper = createHalFormsObjectMapper(this.mapper, this.curieProvider,
this.relProvider, linkRelationMessageSource, this.halFormsConfiguration);
encoders.add(new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
decoders.add(new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
}
encoders.add(new Jackson2JsonEncoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
decoders.add(new Jackson2JsonDecoder(halFormsObjectMapper, MediaTypes.HAL_FORMS_JSON));
}
if (this.hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper, linkRelationMessageSource);
ObjectMapper collectionJsonObjectMapper = createCollectionJsonObjectMapper(this.mapper);
encoders.add(new Jackson2JsonEncoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
decoders.add(new Jackson2JsonDecoder(collectionJsonObjectMapper, MediaTypes.COLLECTION_JSON));
@@ -119,18 +118,16 @@ public class WebClientConfigurer implements BeanFactoryAware {
decoders.add(new Jackson2JsonDecoder(uberObjectMapper, MediaTypes.UBER_JSON));
}
// Include ability to decode into a plain string
encoders.add(CharSequenceEncoder.allMimeTypes());
decoders.add(StringDecoder.allMimeTypes());
return ExchangeStrategies.builder()
.codecs(clientCodecConfigurer -> {
return ExchangeStrategies.builder().codecs(clientCodecConfigurer -> {
encoders.forEach(encoder -> clientCodecConfigurer.customCodecs().encoder(encoder));
decoders.forEach(decoder -> clientCodecConfigurer.customCodecs().decoder(decoder));
encoders.forEach(encoder -> clientCodecConfigurer.customCodecs().encoder(encoder));
decoders.forEach(decoder -> clientCodecConfigurer.customCodecs().decoder(decoder));
clientCodecConfigurer.registerDefaults(false);
})
.build();
clientCodecConfigurer.registerDefaults(false);
}).build();
}
/**
@@ -141,9 +138,6 @@ public class WebClientConfigurer implements BeanFactoryAware {
*/
public WebClient registerHypermediaTypes(WebClient webClient) {
return webClient.mutate()
.exchangeStrategies(hypermediaExchangeStrategies())
.build();
return webClient.mutate().exchangeStrategies(hypermediaExchangeStrategies()).build();
}
}

View File

@@ -1,193 +0,0 @@
/*
* Copyright 2012-2019 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.core;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import org.springframework.core.MethodParameter;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.util.Assert;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.util.ConcurrentReferenceHashMap.ReferenceType;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriTemplate;
/**
* Value object to allow accessing {@link MethodInvocation} parameters with the configured {@link AnnotationAttribute}.
*
* @author Oliver Gierke
*/
@RequiredArgsConstructor
class AnnotatedParametersParameterAccessor {
private static final Map<Method, MethodParameters> METHOD_PARAMETERS_CACHE = new ConcurrentReferenceHashMap<>(
16, ReferenceType.WEAK);
private final @NonNull AnnotationAttribute attribute;
/**
* Returns {@link BoundMethodParameter}s contained in the given {@link MethodInvocation}.
*
* @param invocation must not be {@literal null}.
* @return
*/
public List<BoundMethodParameter> getBoundParameters(MethodInvocation invocation) {
Assert.notNull(invocation, "MethodInvocation must not be null!");
MethodParameters parameters = getOrCreateMethodParametersFor(invocation.getMethod());
Object[] arguments = invocation.getArguments();
List<BoundMethodParameter> result = new ArrayList<>();
for (MethodParameter parameter : parameters.getParametersWith(attribute.getAnnotationType())) {
Object value = arguments[parameter.getParameterIndex()];
Object verifiedValue = verifyParameterValue(parameter, value);
if (verifiedValue != null) {
result.add(createParameter(parameter, verifiedValue, attribute));
}
}
return result;
}
/**
* Create the {@link BoundMethodParameter} for the given {@link MethodParameter}, parameter value and
* {@link AnnotationAttribute}.
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute must not be {@literal null}.
* @return
*/
protected BoundMethodParameter createParameter(MethodParameter parameter, Object value,
AnnotationAttribute attribute) {
return new BoundMethodParameter(parameter, value, attribute);
}
/**
* Callback to verify the parameter values given for a dummy invocation. Default implementation rejects
* {@literal null} values as they indicate an invalid dummy call.
*
* @param parameter will never be {@literal null}.
* @param value could be {@literal null}.
* @return the verified value.
*/
protected Object verifyParameterValue(MethodParameter parameter, Object value) {
return value;
}
/**
* Returns the {@link MethodParameters} for the given {@link Method}.
*
* @param method
* @return
*/
private static MethodParameters getOrCreateMethodParametersFor(Method method) {
return METHOD_PARAMETERS_CACHE.computeIfAbsent(method, MethodParameters::new);
}
/**
* Represents a {@link MethodParameter} alongside the value it has been bound to.
*
* @author Oliver Gierke
*/
static class BoundMethodParameter {
private static final ConversionService CONVERSION_SERVICE = new DefaultFormattingConversionService();
private static final TypeDescriptor STRING_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
private final MethodParameter parameter;
private final Object value;
private final AnnotationAttribute attribute;
private final TypeDescriptor parameterTypeDescriptor;
/**
* Creates a new {@link BoundMethodParameter}
*
* @param parameter must not be {@literal null}.
* @param value can be {@literal null}.
* @param attribute can be {@literal null}.
*/
public BoundMethodParameter(MethodParameter parameter, Object value, AnnotationAttribute attribute) {
Assert.notNull(parameter, "MethodParameter must not be null!");
this.parameter = parameter;
this.value = value;
this.attribute = attribute;
this.parameterTypeDescriptor = TypeDescriptor.nested(parameter, parameter.isOptional() ? 1 : 0);
}
/**
* Returns the name of the {@link UriTemplate} variable to be bound. The name will be derived from the configured
* {@link AnnotationAttribute} or the {@link MethodParameter} name as fallback.
*
* @return
*/
public String getVariableName() {
if (attribute == null) {
return parameter.getParameterName();
}
Annotation annotation = parameter.getParameterAnnotation(attribute.getAnnotationType());
String annotationAttributeValue = attribute.getValueFrom(annotation);
return StringUtils.hasText(annotationAttributeValue) ? annotationAttributeValue : parameter.getParameterName();
}
/**
* Returns the raw value bound to the {@link MethodParameter}.
*
* @return
*/
public Object getValue() {
return value;
}
/**
* Returns the bound value converted into a {@link String} based on default conversion service setup.
*
* @return
*/
public String asString() {
return value == null //
? null //
: (String) CONVERSION_SERVICE.convert(value, parameterTypeDescriptor, STRING_DESCRIPTOR);
}
/**
* Returns whether the given parameter is a required one. Defaults to {@literal true}.
*
* @return
*/
public boolean isRequired() {
return true;
}
}
}

View File

@@ -28,7 +28,7 @@ import org.springframework.hateoas.Link;
import org.springframework.hateoas.MethodLinkBuilderFactory;
import org.springframework.hateoas.core.LinkBuilderSupport;
import org.springframework.hateoas.core.MethodParameters;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.hateoas.WebHandler;
/**
* Factory for {@link LinkBuilderSupport} instances based on the request mapping annotated on the given controller.

View File

@@ -18,9 +18,10 @@ package org.springframework.hateoas.reactive;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.WebHandler;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.core.WebHandler;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
@@ -37,19 +38,18 @@ public class ReactiveLinkBuilder {
private ReactiveLinkBuilder(ControllerLinkBuilder controllerLinkBuilder) {
this.controllerLinkBuilder = controllerLinkBuilder;
}
/**
* Create a {@link ReactiveLinkBuilder} by checking if the Reactor Context contains a {@link ServerWebExchange}
* and using that combined with the Spring Web annotations to build a full URI.
*
* If there is no exchange, then fall back to relative URIs.
* Create a {@link ReactiveLinkBuilder} by checking if the Reactor Context contains a {@link ServerWebExchange} and
* using that combined with the Spring Web annotations to build a full URI. If there is no exchange, then fall back to
* relative URIs.
*
* @param invocationValue
*/
public static Mono<ReactiveLinkBuilder> linkTo(Object invocationValue) {
return Mono.subscriberContext()
.map(context -> linkTo(invocationValue, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
.map(context -> linkTo(invocationValue, context.getOrDefault(SERVER_WEB_EXCHANGE, null)));
}
/**
@@ -61,19 +61,9 @@ public class ReactiveLinkBuilder {
*/
public static ReactiveLinkBuilder linkTo(Object invocationValue, ServerWebExchange exchange) {
ControllerLinkBuilder controllerLinkBuilder = WebHandler.linkTo(invocationValue, path -> {
ControllerLinkBuilder controllerLinkBuilder = WebHandler //
.linkTo(invocationValue, path -> getBuilder(exchange).replacePath(path == null ? "/" : path), null);
UriComponentsBuilder builder = getBuilder(exchange);
if (path == null) {
builder.replacePath("/");
} else {
builder.replacePath(path);
}
return builder;
}, null);
return new ReactiveLinkBuilder(controllerLinkBuilder);
}
@@ -100,10 +90,8 @@ public class ReactiveLinkBuilder {
*/
private static UriComponentsBuilder getBuilder(ServerWebExchange exchange) {
if (exchange == null) {
return UriComponentsBuilder.fromPath("/");
}
return UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
return exchange == null //
? UriComponentsBuilder.fromPath("/") //
: UriComponentsBuilder.fromHttpRequest(exchange.getRequest());
}
}

View File

@@ -17,6 +17,7 @@ package org.springframework.hateoas.reactive;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.hateoas.Resources;
@@ -25,7 +26,7 @@ import org.springframework.web.server.ServerWebExchange;
/**
* Reactive variant of {@link ResourceAssembler} combined with {@link SimpleResourceAssembler}.
*
*
* @author Greg Turnquist
* @since 1.0
*/
@@ -60,14 +61,14 @@ public interface SimpleReactiveResourceAssembler<T> extends ReactiveResourceAsse
*/
default Mono<Resources<Resource<T>>> toResources(Flux<? extends T> entities, ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> {
Resources<Resource<T>> resources = new Resources<>(listOfResources);
addLinks(resources, exchange);
return resources;
});
return entities //
.flatMap(entity -> toResource(entity, exchange)) //
.collectList() //
.map(listOfResources -> {
Resources<Resource<T>> resources = new Resources<>(listOfResources);
addLinks(resources, exchange);
return resources;
});
}
/**
@@ -76,5 +77,4 @@ public interface SimpleReactiveResourceAssembler<T> extends ReactiveResourceAsse
* @param resources
*/
void addLinks(Resources<Resource<T>> resources, ServerWebExchange exchange);
}

View File

@@ -30,6 +30,8 @@ import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.beans.BeanUtils;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils;
@@ -73,6 +75,13 @@ public class PropertyUtils {
public static List<String> findPropertyNames(ResolvableType resolvableType) {
if (WebStack.WEBFLUX.isAvailable()) {
if (Mono.class.equals(resolvableType.getRawClass()) || Flux.class.equals(resolvableType.getRawClass())) {
ResolvableType generic = resolvableType.getGeneric(0);
return findPropertyNames(generic);
}
}
if (resolvableType.getRawClass().equals(Resource.class)) {
return findPropertyNames(resolvableType.resolveGeneric(0));
} else {

View File

@@ -39,7 +39,7 @@ public class ArchitectureTest {
public void assertNoCyclicPackageDependencies() {
SliceRule rule = SlicesRuleDefinition.slices() //
.matching("org.springframework.(**)..") //
.matching("org.springframework.hateoas.(**)..") //
.should().beFreeOfCycles();
rule.check(classes);

View File

@@ -25,14 +25,18 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.support.Employee;
import org.springframework.hateoas.support.MappingUtils;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
@@ -51,6 +55,7 @@ public class CollectionJsonSpecTest {
public void setUp() {
mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
}
@@ -203,9 +208,13 @@ public class CollectionJsonSpecTest {
@Test
public void specPart7() throws IOException {
String specBasedJson = MappingUtils.read(new ClassPathResource("spec-part7.json", getClass()));
String specBasedJson = MappingUtils.read(new ClassPathResource("spec-part7-adjusted.json", getClass()));
// TODO: Come up with a way to verify this JSON spec can be used to create a new resource.
Resource<Employee> resource = mapper.readValue(specBasedJson,
mapper.getTypeFactory().constructParametricType(Resource.class, Employee.class));
assertThat(resource.getContent()).isEqualTo(new Employee("W. Chandry", "developer"));
assertThat(resource.getLinks()).isEmpty();
}
@Data

View File

@@ -0,0 +1,200 @@
/*
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.collectionjson;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.reactive.WebClientConfigurer;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* @author Greg Turnquist
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class CollectionJsonWebFluxIntegrationTest {
@Autowired WebTestClient testClient;
@Before
public void setUp() {
WebFluxEmployeeController.reset();
}
/**
* @see #728
*/
@Test
public void singleEmployee() {
this.testClient.get().uri("http://localhost/employees/0")
.accept(MediaTypes.COLLECTION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.COLLECTION_JSON)
.expectBody(String.class)
.value(jsonPath("$.collection.version", is("1.0")))
.value(jsonPath("$.collection.href", is("http://localhost/employees/0")))
.value(jsonPath("$.collection.links.*", hasSize(1)))
.value(jsonPath("$.collection.links[0].rel", is("employees")))
.value(jsonPath("$.collection.links[0].href", is("http://localhost/employees")))
.value(jsonPath("$.collection.items.*", hasSize(1)))
.value(jsonPath("$.collection.items[0].data[1].name", is("name")))
.value(jsonPath("$.collection.items[0].data[1].value", is("Frodo Baggins")))
.value(jsonPath("$.collection.items[0].data[0].name", is("role")))
.value(jsonPath("$.collection.items[0].data[0].value", is("ring bearer")))
.value(jsonPath("$.collection.items[0].links.*", hasSize(1)))
.value(jsonPath("$.collection.items[0].links[0].rel", is("employees")))
.value(jsonPath("$.collection.items[0].links[0].href", is("http://localhost/employees")))
.value(jsonPath("$.collection.template.*", hasSize(1)))
.value(jsonPath("$.collection.template.data[0].name", is("name")))
.value(jsonPath("$.collection.template.data[0].value", is("")))
.value(jsonPath("$.collection.template.data[1].name", is("role")))
.value(jsonPath("$.collection.template.data[1].value", is("")));
}
/**
* @see #728
*/
@Test
public void collectionOfEmployees() {
this.testClient.get().uri("http://localhost/employees")
.accept(MediaTypes.COLLECTION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.COLLECTION_JSON)
.expectBody(String.class)
.value(jsonPath("$.collection.version", is("1.0")))
.value(jsonPath("$.collection.href", is("http://localhost/employees")))
.value(jsonPath("$.collection.items.*", hasSize(2)))
.value(jsonPath("$.collection.items[0].data[1].name", is("name")))
.value(jsonPath("$.collection.items[0].data[1].value", is("Frodo Baggins")))
.value(jsonPath("$.collection.items[0].data[0].name", is("role")))
.value(jsonPath("$.collection.items[0].data[0].value", is("ring bearer")))
.value(jsonPath("$.collection.items[0].links.*", hasSize(1)))
.value(jsonPath("$.collection.items[0].links[0].rel", is("employees")))
.value(jsonPath("$.collection.items[0].links[0].href", is("http://localhost/employees")))
.value(jsonPath("$.collection.items[1].data[1].name", is("name")))
.value(jsonPath("$.collection.items[1].data[1].value", is("Bilbo Baggins")))
.value(jsonPath("$.collection.items[1].data[0].name", is("role")))
.value(jsonPath("$.collection.items[1].data[0].value", is("burglar")))
.value(jsonPath("$.collection.items[1].links.*", hasSize(1)))
.value(jsonPath("$.collection.items[1].links[0].rel", is("employees")))
.value(jsonPath("$.collection.items[1].links[0].href", is("http://localhost/employees")))
.value(jsonPath("$.collection.template.*", hasSize(1)))
.value(jsonPath("$.collection.template.data[0].name", is("name")))
.value(jsonPath("$.collection.template.data[0].value", is("")))
.value(jsonPath("$.collection.template.data[1].name", is("role")))
.value(jsonPath("$.collection.template.data[1].value", is("")));
}
/**
* @see #728
*/
@Test
public void createNewEmployee() throws Exception {
String specBasedJson = read(new ClassPathResource("spec-part7-adjusted.json", getClass()));
this.testClient.post().uri("http://localhost/employees")
.contentType(MediaTypes.COLLECTION_JSON)
.syncBody(specBasedJson)
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2");
this.testClient.get().uri("http://localhost/employees/2")
.accept(MediaTypes.COLLECTION_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.COLLECTION_JSON)
.expectBody(String.class)
.value(jsonPath("$.collection.version", is("1.0")))
.value(jsonPath("$.collection.href", is("http://localhost/employees/2")))
.value(jsonPath("$.collection.links.*", hasSize(1)))
.value(jsonPath("$.collection.links[0].rel", is("employees")))
.value(jsonPath("$.collection.links[0].href", is("http://localhost/employees")))
.value(jsonPath("$.collection.items.*", hasSize(1)))
.value(jsonPath("$.collection.items[0].data[1].name", is("name")))
.value(jsonPath("$.collection.items[0].data[1].value", is("W. Chandry")))
.value(jsonPath("$.collection.items[0].data[0].name", is("role")))
.value(jsonPath("$.collection.items[0].data[0].value", is("developer")))
.value(jsonPath("$.collection.items[0].links.*", hasSize(1)))
.value(jsonPath("$.collection.items[0].links[0].rel", is("employees")))
.value(jsonPath("$.collection.items[0].links[0].href", is("http://localhost/employees")))
.value(jsonPath("$.collection.template.*", hasSize(1)))
.value(jsonPath("$.collection.template.data[0].name", is("name")))
.value(jsonPath("$.collection.template.data[0].value", is("")))
.value(jsonPath("$.collection.template.data[1].name", is("role")))
.value(jsonPath("$.collection.template.data[1].value", is("")));
}
@Configuration
@EnableWebFlux
@EnableHypermediaSupport(type = { HypermediaType.COLLECTION_JSON })
static class TestConfig {
@Bean
WebFluxEmployeeController employeeController() {
return new WebFluxEmployeeController();
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
}
}
}

View File

@@ -25,10 +25,10 @@ import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.junit.Before;
import org.junit.Test;
@@ -46,6 +46,7 @@ import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.support.Employee;
import org.springframework.hateoas.support.MappingUtils;
import org.springframework.hateoas.support.WebMvcEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
@@ -75,17 +76,11 @@ public class CollectionJsonWebMvcIntegrationTest {
MockMvc mockMvc;
private static Map<Integer, Employee> EMPLOYEES;
@Before
public void setUp() {
this.mockMvc = webAppContextSetup(this.context).build();
EMPLOYEES = new TreeMap<>();
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
WebMvcEmployeeController.reset();
}
@Test
@@ -196,6 +191,8 @@ public class CollectionJsonWebMvcIntegrationTest {
@RestController
static class EmployeeController {
private static Map<Integer, Employee> EMPLOYEES = new HashMap<>();
@GetMapping("/employees")
public Resources<Resource<Employee>> all() {
@@ -334,8 +331,8 @@ public class CollectionJsonWebMvcIntegrationTest {
static class TestConfig {
@Bean
EmployeeController employeeController() {
return new EmployeeController();
WebMvcEmployeeController employeeController() {
return new WebMvcEmployeeController();
}
}
}

View File

@@ -18,13 +18,14 @@ package org.springframework.hateoas.config.reactive;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.support.ContextTester.*;
import reactor.test.StepVerifier;
import java.io.IOException;
import java.net.URI;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import reactor.test.StepVerifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
@@ -52,7 +53,7 @@ public class HypermediaWebClientBeanPostProcessorTest {
public void setUp() {
this.server = new Server();
Resource<Actor> actor = new Resource<>(new Actor("Keanu Reaves"));
String actorUri = this.server.mockResourceFor(actor);
@@ -67,7 +68,7 @@ public class HypermediaWebClientBeanPostProcessorTest {
}
@After
public void tearDown() throws IOException {
public void tearDown() {
if (this.server != null) {
this.server.close();
@@ -81,18 +82,17 @@ public class HypermediaWebClientBeanPostProcessorTest {
WebClient webClient = context.getBean(WebClient.class);
webClient
.get().uri(this.baseUri)
.accept(MediaTypes.HAL_JSON)
.retrieve()
.bodyToMono(ResourceSupport.class)
.as(StepVerifier::create)
.expectNextMatches(root -> {
assertThat(root.getLinks()).hasSize(2);
return true;
webClient //
.get().uri(this.baseUri) //
.accept(MediaTypes.HAL_JSON) //
.retrieve() //
.bodyToMono(ResourceSupport.class) //
.as(StepVerifier::create) //
.expectNextMatches(root -> { //
assertThat(root.getLinks()).hasSize(2);
return true;
})
.verifyComplete();
}).verifyComplete();
});
}
@@ -105,23 +105,23 @@ public class HypermediaWebClientBeanPostProcessorTest {
WebClient webClient = context.getBean(WebClient.class);
webClient
.get().uri(this.baseUri)
.retrieve()
.bodyToMono(ResourceSupport.class)
.map(resourceSupport -> resourceSupport.getLink("actors").orElseThrow(() -> new RuntimeException("Link not found!")))
.flatMap(link -> webClient
.get().uri(link.expand().getHref())
.retrieve()
.bodyToMono(ResourceSupport.class))
.map(resourceSupport -> resourceSupport.getLinks().get(0))
.flatMap(link -> webClient
.get().uri(link.expand().getHref())
.retrieve()
.bodyToMono(typeReference))
.as(StepVerifier::create)
.expectNext(new Resource<>(new Actor("Keanu Reaves")))
.verifyComplete();
webClient //
.get().uri(this.baseUri) //
.retrieve() //
.bodyToMono(ResourceSupport.class) //
.map(resourceSupport -> resourceSupport.getRequiredLink("actors")) //
.flatMap(link -> webClient //
.get().uri(link.expand().getHref()) //
.retrieve() //
.bodyToMono(ResourceSupport.class)) //
.map(resourceSupport -> resourceSupport.getLinks().toList().get(0)) //
.flatMap(link -> webClient //
.get().uri(link.expand().getHref()) //
.retrieve() //
.bodyToMono(typeReference)) //
.as(StepVerifier::create) //
.expectNext(new Resource<>(new Actor("Keanu Reaves"))) //
.verifyComplete();
});
}
@@ -134,4 +134,4 @@ public class HypermediaWebClientBeanPostProcessorTest {
return WebClient.create();
}
}
}
}

View File

@@ -15,22 +15,24 @@
*/
package org.springframework.hateoas.config.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat;
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
@@ -69,12 +71,13 @@ public class HypermediaWebFluxConfigurerTest {
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
}
/**
* @see #728
*/
@Test
public void registeringHalShouldServeHal() {
@@ -83,8 +86,14 @@ public class HypermediaWebFluxConfigurerTest {
verifyRootUriServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityWorks(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
}
/**
* @see #728
*/
@Test
public void registeringHalFormsShouldServeHalForms() {
@@ -93,8 +102,14 @@ public class HypermediaWebFluxConfigurerTest {
verifyRootUriServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityWorks(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_FORMS_JSON);
}
/**
* @see #728
*/
@Test
public void registeringCollectionJsonShouldServerCollectionJson() {
@@ -103,8 +118,14 @@ public class HypermediaWebFluxConfigurerTest {
verifyRootUriServesHypermedia(MediaTypes.COLLECTION_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.COLLECTION_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.COLLECTION_JSON);
verifyCreatingNewEntityWorks(MediaTypes.COLLECTION_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.COLLECTION_JSON);
}
/**
* @see #728
*/
@Test
public void registeringUberShouldServerUber() {
@@ -113,8 +134,14 @@ public class HypermediaWebFluxConfigurerTest {
verifyRootUriServesHypermedia(MediaTypes.UBER_JSON);
verifyAggregateRootServesHypermedia(MediaTypes.UBER_JSON);
verifySingleItemResourceServesHypermedia(MediaTypes.UBER_JSON);
verifyCreatingNewEntityWorks(MediaTypes.UBER_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.UBER_JSON);
}
/**
* @see #728
*/
@Test
public void registeringHalAndHalFormsShouldServerHalAndHalForms() {
@@ -129,6 +156,24 @@ public class HypermediaWebFluxConfigurerTest {
verifySingleItemResourceServesHypermedia(MediaTypes.HAL_FORMS_JSON);
}
/**
* @see #728
*/
@Test
public void registeringHalAndHalFormsShouldAllowCreatingViaHalAndHalForms() {
setUp(AllHalWebFluxConfig.class);
verifyCreatingNewEntityWorks(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityWorks(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_FORMS_JSON);
}
/**
* @see #728
*/
@Test
public void registeringHalAndCollectionJsonShouldServerHalAndCollectionJson() {
@@ -147,6 +192,27 @@ public class HypermediaWebFluxConfigurerTest {
verifySingleItemResourceServesHypermedia(MediaTypes.COLLECTION_JSON);
}
/**
* @see #728
*/
@Test
public void registeringHalAndCollectionJsonShouldAllowCreatingViaHalAndCollectionJson() {
setUp(HalAndCollectionJsonWebFluxConfig.class);
verifyCreatingNewEntityWorks(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityWorks(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityWorks(MediaTypes.COLLECTION_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.COLLECTION_JSON);
}
/**
* @see #728
*/
@Test
public void registeringAllHypermediaTypesShouldServerThemAll() {
@@ -169,90 +235,87 @@ public class HypermediaWebFluxConfigurerTest {
verifySingleItemResourceServesHypermedia(MediaTypes.UBER_JSON);
}
/**
* @see #728
*/
@Test
public void registeringAllHypermediaTypesShouldAllowCreatingThroughAllFormats() {
setUp(AllHypermediaTypesWebFluxConfig.class);
verifyCreatingNewEntityWorks(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
verifyCreatingNewEntityWorks(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.HAL_FORMS_JSON);
verifyCreatingNewEntityWorks(MediaTypes.COLLECTION_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.COLLECTION_JSON);
verifyCreatingNewEntityWorks(MediaTypes.UBER_JSON);
verifyCreatingNewEntityReactivelyShouldWork(MediaTypes.UBER_JSON);
}
/**
* @see #728
*/
@Test
public void callingForUnregisteredMediaTypeShouldFail() {
setUp(HalWebFluxConfig.class);
this.testClient.get().uri("/")
.accept(MediaTypes.UBER_JSON)
.exchange()
.expectStatus().is4xxClientError()
.returnResult(String.class)
.getResponseBody()
.as(StepVerifier::create)
.verifyComplete();
this.testClient.get().uri("/").accept(MediaTypes.UBER_JSON).exchange().expectStatus().is4xxClientError()
.returnResult(String.class).getResponseBody().as(StepVerifier::create).verifyComplete();
}
/**
* @see #728
*/
@Test
public void reactorTypesShouldWork() {
setUp(HalWebFluxConfig.class);
this.testClient.get().uri("/reactive")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(ResourceSupport.class)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
this.testClient.get().uri("/reactive").accept(MediaTypes.HAL_JSON).exchange() //
.expectStatus().isOk() //
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8) //
.returnResult(ResourceSupport.class).getResponseBody().as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
assertThat(resourceSupport.getLinks()).containsExactlyInAnyOrder(
new Link("/", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees")
);
return true;
})
.verifyComplete();
assertThat(resourceSupport.getLinks()).containsExactlyInAnyOrder(new Link("/", IanaLinkRelations.SELF),
new Link("/employees", "employees"));
this.testClient.get().uri("/reactive/employees")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(this.resourcesEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resources -> {
return true;
}).verifyComplete();
assertThat(resources.getContent()).extracting("content").containsExactlyInAnyOrder(
new Employee("Frodo Baggins", "ring bearer"));
this.testClient.get().uri("/reactive/employees").accept(MediaTypes.HAL_JSON).exchange() //
.expectStatus().isOk().expectHeader().contentType(MediaTypes.HAL_JSON_UTF8) //
.returnResult(this.resourcesEmployeeType).getResponseBody() //
.as(StepVerifier::create).expectNextMatches(resources -> {
assertThat(resources.getContent()).extracting("links").containsExactlyInAnyOrder(
Arrays.asList(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees")));
assertThat(resources.getLinks()).containsExactlyInAnyOrder(new Link("/employees", IanaLinkRelations.SELF));
assertThat(resources.getLinks()).containsExactlyInAnyOrder(
new Link("/employees", IanaLinkRelation.SELF.value()));
Resource<Employee> content = resources.getContent().iterator().next();
return true;
})
.verifyComplete();
assertThat(content.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer"));
assertThat(content.getLinks()) //
.containsExactlyInAnyOrder(new Link("/employees/1", IanaLinkRelations.SELF),
new Link("/employees", "employees"));
this.testClient.get().uri("/reactive/employees/1")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(this.resourceEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(employee -> {
System.out.println(employee);
return true;
}).verifyComplete();
assertThat(employee.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer"));
assertThat(employee.getLinks()).containsExactlyInAnyOrder(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees")
);
return true;
})
.verifyComplete();
this.testClient.get().uri("/reactive/employees/1").accept(MediaTypes.HAL_JSON).exchange() //
.expectStatus().isOk().expectHeader().contentType(MediaTypes.HAL_JSON_UTF8) //
.returnResult(this.resourceEmployeeType).getResponseBody() //
.as(StepVerifier::create).expectNextMatches(employee -> {
assertThat(employee.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer"));
assertThat(employee.getLinks()) //
.containsExactlyInAnyOrder(new Link("/employees/1", IanaLinkRelations.SELF),
new Link("/employees", "employees"));
return true;
}).verifyComplete();
}
private void verifyRootUriServesHypermedia(MediaType mediaType) {
@@ -261,23 +324,17 @@ public class HypermediaWebFluxConfigurerTest {
private void verifyRootUriServesHypermedia(MediaType requestType, MediaType responseType) {
this.testClient.get().uri("/")
.accept(requestType)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(responseType)
.returnResult(ResourceSupport.class)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
this.testClient.get().uri("/").accept(requestType).exchange() //
.expectStatus().isOk() //
.expectHeader().contentType(responseType) //
.returnResult(ResourceSupport.class).getResponseBody().as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
assertThat(resourceSupport.getLinks()).containsExactlyInAnyOrder(
new Link("/", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees"));
assertThat(resourceSupport.getLinks()) //
.containsExactlyInAnyOrder(new Link("/", IanaLinkRelations.SELF), new Link("/employees", "employees"));
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
private void verifyAggregateRootServesHypermedia(MediaType mediaType) {
@@ -286,35 +343,24 @@ public class HypermediaWebFluxConfigurerTest {
private void verifyAggregateRootServesHypermedia(MediaType requestType, MediaType responseType) {
this.testClient.get().uri("/employees")
.accept(requestType)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(responseType)
.returnResult(this.resourcesEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resources -> {
this.testClient.get().uri("/employees").accept(requestType).exchange().expectStatus().isOk().expectHeader()
.contentType(responseType).returnResult(this.resourcesEmployeeType).getResponseBody().as(StepVerifier::create)
.expectNextMatches(resources -> {
assertThat(resources.getContent()).hasSize(1);
assertThat(resources.getLinks()).containsExactlyInAnyOrder(new Link("/employees", IanaLinkRelations.SELF));
assertThat(resources.getContent()).extracting("content").containsExactlyInAnyOrder(
new Employee("Frodo Baggins", "ring bearer")
);
Collection<Resource<Employee>> content = resources.getContent();
assertThat(content).hasSize(1);
assertThat(resources.getContent()).extracting("links").containsExactlyInAnyOrder(
Arrays.asList(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees"))
);
Resource<Employee> resource = content.iterator().next();
assertThat(resources.getLinks()).containsExactlyInAnyOrder(
new Link("/employees", IanaLinkRelation.SELF.value())
);
assertThat(resource.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer"));
assertThat(resource.getLinks()) //
.containsExactlyInAnyOrder(new Link("/employees/1", IanaLinkRelations.SELF),
new Link("/employees", "employees"));
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
private void verifySingleItemResourceServesHypermedia(MediaType mediaType) {
@@ -323,28 +369,56 @@ public class HypermediaWebFluxConfigurerTest {
private void verifySingleItemResourceServesHypermedia(MediaType requestType, MediaType responseType) {
this.testClient.get().uri("/employees/1")
.accept(requestType)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(responseType)
.returnResult(this.resourceEmployeeType)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(employeeResource -> {
this.testClient.get().uri("/employees/1") //
.accept(requestType).exchange() //
.expectStatus().isOk() //
.expectHeader().contentType(responseType) //
.returnResult(this.resourceEmployeeType).getResponseBody().as(StepVerifier::create) //
.expectNextMatches(employeeResource -> {
assertThat(employeeResource.getContent()).isEqualTo(
new Employee("Frodo Baggins", "ring bearer"));
assertThat(employeeResource.getContent()).isEqualTo(new Employee("Frodo Baggins", "ring bearer"));
assertThat(employeeResource.getLinks()).containsExactlyInAnyOrder(
new Link("/employees/1", IanaLinkRelations.SELF), new Link("/employees", "employees"));
assertThat(employeeResource.getLinks()).containsExactlyInAnyOrder(
new Link("/employees/1", IanaLinkRelation.SELF.value()),
new Link("/employees", "employees"));
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
private void verifyCreatingNewEntityWorks(MediaType mediaType) {
verifyCreatingNewEntityWorks(mediaType, mediaType);
}
private void verifyCreatingNewEntityWorks(MediaType contentType, MediaType responseType) {
verifyCreation("/employees", contentType, responseType);
}
private void verifyCreatingNewEntityReactivelyShouldWork(MediaType contentType) {
verifyCreatingNewEntityReactivelyShouldWork(contentType, contentType);
}
private void verifyCreatingNewEntityReactivelyShouldWork(MediaType contentType, MediaType responseType) {
verifyCreation("/reactive/employees", contentType, responseType);
}
private void verifyCreation(String uri, MediaType contentType, MediaType responseType) {
this.testClient.post().uri(uri) //
.accept(contentType).contentType(contentType)
.body(Mono.just(new Employee("Samwise Gamgee", "gardener")), Employee.class) //
.exchange() //
.expectStatus().isOk() //
.expectHeader().contentType(responseType).returnResult(this.resourceEmployeeType) //
.getResponseBody().as(StepVerifier::create).expectNextMatches(resource -> {
assertThat(resource.getContent()).isEqualTo(new Employee("Samwise Gamgee", "gardener"));
assertThat(resource.getLinks()) //
.containsExactlyInAnyOrder(new Link("/employees/1", IanaLinkRelations.SELF),
new Link("/employees", "employees"));
return true;
}).verifyComplete();
}
@Configuration
@EnableWebFlux
static abstract class BaseConfig {
@@ -356,32 +430,25 @@ public class HypermediaWebFluxConfigurerTest {
}
@EnableHypermediaSupport(type = HAL)
static class HalWebFluxConfig extends BaseConfig {
}
static class HalWebFluxConfig extends BaseConfig {}
@EnableHypermediaSupport(type = HAL_FORMS)
static class HalFormsWebFluxConfig extends BaseConfig {
}
static class HalFormsWebFluxConfig extends BaseConfig {}
@EnableHypermediaSupport(type = COLLECTION_JSON)
static class CollectionJsonWebFluxConfig extends BaseConfig {
}
static class CollectionJsonWebFluxConfig extends BaseConfig {}
@EnableHypermediaSupport(type = UBER)
static class UberWebFluxConfig extends BaseConfig {
}
static class UberWebFluxConfig extends BaseConfig {}
@EnableHypermediaSupport(type = {HAL, HAL_FORMS})
static class AllHalWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = { HAL, HAL_FORMS })
static class AllHalWebFluxConfig extends BaseConfig {}
@EnableHypermediaSupport(type = {HAL, HAL_FORMS, COLLECTION_JSON})
static class HalAndCollectionJsonWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = { HAL, HAL_FORMS, COLLECTION_JSON })
static class HalAndCollectionJsonWebFluxConfig extends BaseConfig {}
@EnableHypermediaSupport(type = {HAL, HAL_FORMS, COLLECTION_JSON, UBER})
static class AllHypermediaTypesWebFluxConfig extends BaseConfig {
}
@EnableHypermediaSupport(type = { HAL, HAL_FORMS, COLLECTION_JSON, UBER })
static class AllHypermediaTypesWebFluxConfig extends BaseConfig {}
@RestController
static class TestController {
@@ -390,14 +457,14 @@ public class HypermediaWebFluxConfigurerTest {
private EmployeeResourceAssembler assembler = new EmployeeResourceAssembler();
TestController() {
this.employees = new ArrayList<>();
this.employees.add(new Employee("Frodo Baggins", "ring bearer"));
}
@GetMapping
ResourceSupport root() {
ResourceSupport root = new ResourceSupport();
root.add(new Link("/").withSelfRel());
@@ -418,7 +485,7 @@ public class HypermediaWebFluxConfigurerTest {
return this.assembler.toResource(newEmployee);
}
@GetMapping("/employees/{id}")
Resource<Employee> employee(@PathVariable String id) {
return this.assembler.toResource(this.employees.get(0));
@@ -440,15 +507,24 @@ public class HypermediaWebFluxConfigurerTest {
@GetMapping("/reactive/employees")
Mono<Resources<Resource<Employee>>> reactiveEmployees() {
return findAll()
.collectList()
.map(employees -> this.assembler.toResources(employees));
return findAll() //
.collectList() //
.map(assembler::toResources);
}
@PostMapping("/reactive/employees")
Mono<Resource<Employee>> createReactiveEmployee(@RequestBody Mono<Employee> newEmployee) {
return newEmployee.map(employee -> {
employees.add(employee);
return employee;
}).map(assembler::toResource);
}
@GetMapping("/reactive/employees/{id}")
Mono<Resource<Employee>> reactiveEmployee(@PathVariable String id) {
return findById(0)
.map(this.assembler::toResource);
return findById(0) //
.map(assembler::toResource);
}
Mono<Employee> findById(int id) {
@@ -474,4 +550,5 @@ public class HypermediaWebFluxConfigurerTest {
resources.add(new Link("/employees").withSelfRel());
}
}
}
}

View File

@@ -0,0 +1,156 @@
/*
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.hal.forms;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.reactive.WebClientConfigurer;
import org.springframework.hateoas.support.MappingUtils;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* @author Greg Turnquist
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class HalFormsWebFluxIntegrationTest {
@Autowired WebTestClient testClient;
@Before
public void setUp() {
WebFluxEmployeeController.reset();
}
/**
* @see #728
*/
@Test
public void singleEmployee() {
this.testClient.get().uri("http://localhost/employees/0")
.accept(MediaTypes.HAL_FORMS_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_FORMS_JSON)
.expectBody(String.class)
.value(jsonPath("$.name", is("Frodo Baggins")))
.value(jsonPath("$.role", is("ring bearer")))
.value(jsonPath("$._links.*", hasSize(2)))
.value(jsonPath("$._links['self'].href", is("http://localhost/employees/0")))
.value(jsonPath("$._links['employees'].href", is("http://localhost/employees")))
.value(jsonPath("$._templates.*", hasSize(2)))
.value(jsonPath("$._templates['default'].method", is("put")))
.value(jsonPath("$._templates['default'].properties[0].name", is("name")))
.value(jsonPath("$._templates['default'].properties[0].required", is(true)))
.value(jsonPath("$._templates['default'].properties[1].name", is("role")))
.value(jsonPath("$._templates['default'].properties[1].required", is(true)))
.value(jsonPath("$._templates['partiallyUpdateEmployee'].method", is("patch")))
.value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].name", is("name")))
.value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[0].required", is(false)))
.value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].name", is("role")))
.value(jsonPath("$._templates['partiallyUpdateEmployee'].properties[1].required", is(false)));
}
/**
* @see #728
*/
@Test
public void collectionOfEmployees() {
this.testClient.get().uri("http://localhost/employees")
.accept(MediaTypes.HAL_FORMS_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_FORMS_JSON)
.expectBody(String.class)
.value(jsonPath("$._embedded.employees[0].name", is("Frodo Baggins")))
.value(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.value(jsonPath("$._embedded.employees[0]._links['self'].href", is("http://localhost/employees/0")))
.value(jsonPath("$._embedded.employees[1].name", is("Bilbo Baggins")))
.value(jsonPath("$._embedded.employees[1].role", is("burglar")))
.value(jsonPath("$._embedded.employees[1]._links['self'].href", is("http://localhost/employees/1")))
.value(jsonPath("$._links.*", hasSize(1)))
.value(jsonPath("$._links['self'].href", is("http://localhost/employees")))
.value(jsonPath("$._templates.*", hasSize(1)))
.value(jsonPath("$._templates['default'].method", is("post")))
.value(jsonPath("$._templates['default'].properties[0].name", is("name")))
.value(jsonPath("$._templates['default'].properties[0].required", is(true)))
.value(jsonPath("$._templates['default'].properties[1].name", is("role")))
.value(jsonPath("$._templates['default'].properties[1].required", is(true)));
}
/**
* @see #728
*/
@Test
public void createNewEmployee() throws Exception {
String specBasedJson = MappingUtils.read(new ClassPathResource("new-employee.json", getClass()));
this.testClient.post().uri("http://localhost/employees")
.contentType(MediaTypes.HAL_FORMS_JSON)
.syncBody(specBasedJson)
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2");
}
@Configuration
@EnableWebFlux
@EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS })
static class TestConfig {
@Bean
WebFluxEmployeeController employeeController() {
return new WebFluxEmployeeController();
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
}
}
}

View File

@@ -17,18 +17,10 @@ package org.springframework.hateoas.hal.forms;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -39,25 +31,15 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.support.Employee;
import org.springframework.hateoas.support.MappingUtils;
import org.springframework.hateoas.support.WebMvcEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -75,7 +57,9 @@ public class HalFormsWebMvcIntegrationTest {
@Before
public void setUp() {
this.mockMvc = webAppContextSetup(this.context).build();
WebMvcEmployeeController.reset();
}
@Test
@@ -138,117 +122,14 @@ public class HalFormsWebMvcIntegrationTest {
.andExpect(header().stringValues(HttpHeaders.LOCATION, "http://localhost/employees/2"));
}
@RestController
static class EmployeeController {
private final static Map<Integer, Employee> EMPLOYEES = new TreeMap<>();
static {
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
}
@GetMapping("/employees")
public Resources<Resource<Employee>> all() {
// Create a list of Resource<Employee>'s to return
List<Resource<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
employees.add(findOne(i));
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(EmployeeController.class).all()).withSelfRel() //
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null)));
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
}
@GetMapping("/employees/{id}")
public Resource<Employee> findOne(@PathVariable Integer id) {
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
// Define final link as means to find entire collection.
Link employeesLink = linkTo(methodOn(EmployeeController.class).all()).withRel("employees");
// Return the affordance + a link back to the entire collection resource.
return new Resource<>(EMPLOYEES.get(id), //
findOneLink.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))) //
.andAffordance(afford(methodOn(EmployeeController.class).partiallyUpdateEmployee(null, id))),
employeesLink);
}
@PostMapping("/employees")
public ResponseEntity<?> newEmployee(@RequestBody Employee employee) {
int newEmployeeId = EMPLOYEES.size();
EMPLOYEES.put(newEmployeeId, employee);
try {
return ResponseEntity.created(toUri(newEmployeeId)).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable Integer id) {
EMPLOYEES.put(id, employee);
try {
return ResponseEntity.noContent().location(toUri(id)).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable Integer id) {
Employee newEmployee = EMPLOYEES.get(id);
if (employee.getName() != null) {
newEmployee = newEmployee.withName(employee.getName());
}
if (employee.getRole() != null) {
newEmployee = newEmployee.withRole(employee.getRole());
}
EMPLOYEES.put(id, newEmployee);
try {
return ResponseEntity.noContent().location(toUri(id)).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
private URI toUri(Integer id) throws URISyntaxException {
String uri = findOne(id).getLink(IanaLinkRelations.SELF.value()) //
.map(link -> link.expand().getHref()) //
.orElse("");
return new URI(uri);
}
}
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS })
static class TestConfig {
@Bean
EmployeeController employeeController() {
return new EmployeeController();
WebMvcEmployeeController employeeController() {
return new WebMvcEmployeeController();
}
}
}

View File

@@ -20,14 +20,15 @@ import static org.springframework.hateoas.config.EnableHypermediaSupport.Hyperme
import static org.springframework.hateoas.core.DummyInvocationUtils.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.*;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.ResourceSupport;
@@ -45,7 +46,7 @@ import org.springframework.web.reactive.config.EnableWebFlux;
public class HypermediaWebFilterTest {
WebTestClient testClient;
@Before
public void setUp() {
@@ -55,32 +56,28 @@ public class HypermediaWebFilterTest {
WebClientConfigurer webClientConfigurer = ctx.getBean(WebClientConfigurer.class);
this.testClient = WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
this.testClient = WebTestClient.bindToApplicationContext(ctx).build().mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()).build();
}
@Test
public void webFilterShouldEmbedExchangeIntoContext() {
this.testClient.get().uri("http://example.com/api")
.accept(MediaTypes.HAL_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8)
.returnResult(ResourceSupport.class)
.getResponseBody()
.as(StepVerifier::create)
.expectNextMatches(resourceSupport -> {
this.testClient.get().uri("http://example.com/api") //
.accept(MediaTypes.HAL_JSON) //
.exchange() //
.expectStatus().isOk() //
.expectHeader().contentType(MediaTypes.HAL_JSON_UTF8) //
.returnResult(ResourceSupport.class) //
.getResponseBody() //
.as(StepVerifier::create) //
.expectNextMatches(resourceSupport -> {
assertThat(resourceSupport.getLinks()).containsExactly(
new Link("http://example.com/api", IanaLinkRelation.SELF.value()));
return true;
})
.verifyComplete();
assertThat(resourceSupport.getLinks())//
.containsExactly(new Link("http://example.com/api", IanaLinkRelations.SELF));
return true;
}).verifyComplete();
}
@RestController
@@ -90,9 +87,8 @@ public class HypermediaWebFilterTest {
@GetMapping
Mono<ResourceSupport> root() {
return linkTo(methodOn(TestController.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.map(ResourceSupport::new);
return linkTo(methodOn(TestController.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.map(ResourceSupport::new);
}
}

View File

@@ -21,6 +21,10 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.reactive.HypermediaWebFilter.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.linkTo;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
import java.net.URI;
import java.net.URISyntaxException;
@@ -28,10 +32,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import reactor.util.context.Context;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -59,7 +60,7 @@ public class ReactiveLinkBuilderTest {
Link link = linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
}
@@ -71,18 +72,15 @@ public class ReactiveLinkBuilderTest {
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
})
.verifyComplete();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
}).verifyComplete();
}
@Test
@@ -95,7 +93,7 @@ public class ReactiveLinkBuilderTest {
Link link = linkTo(methodOn(TestController.class).root(), this.exchange).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
}
@@ -107,31 +105,28 @@ public class ReactiveLinkBuilderTest {
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api/employees"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api");
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
@Test
public void deepLinkFromShallowExplicitServerExchangeShouldWork() throws URISyntaxException {
when(this.exchange.getRequest()).thenReturn(this.request);
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
Link link = linkTo(methodOn(TestController.class).deep(), this.exchange).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
}
@@ -143,18 +138,15 @@ public class ReactiveLinkBuilderTest {
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/api"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController.class).deep())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController.class).deep()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
return true;
})
.verifyComplete();
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/api/employees");
return true;
}).verifyComplete();
}
@Test
@@ -165,34 +157,30 @@ public class ReactiveLinkBuilderTest {
when(this.request.getURI()).thenReturn(new URI("http://localhost:8080/"));
when(this.request.getHeaders()).thenReturn(new HttpHeaders());
linkTo(methodOn(TestController2.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange))
.as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController2.class).root()).map(ReactiveLinkBuilder::withSelfRel)
.subscriberContext(Context.of(SERVER_WEB_EXCHANGE, this.exchange)).as(StepVerifier::create)
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("http://localhost:8080/");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("http://localhost:8080/");
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
@Test
public void linkToRouteWithNoExchangeInTheContextShouldFallbackToRelativeUris() throws URISyntaxException {
linkTo(methodOn(TestController2.class).root())
.map(ReactiveLinkBuilder::withSelfRel)
.as(StepVerifier::create)
.expectNextMatches(link -> {
linkTo(methodOn(TestController2.class).root())//
.map(ReactiveLinkBuilder::withSelfRel) //
.as(StepVerifier::create) //
.expectNextMatches(link -> {
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getHref()).isEqualTo("/");
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("/");
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
@Test
@@ -200,11 +188,10 @@ public class ReactiveLinkBuilderTest {
Link link = linkTo(methodOn(TestController2.class).root(), null).withSelfRel();
assertThat(link.getRel()).isEqualTo(IanaLinkRelation.SELF.value());
assertThat(link.getRel()).isEqualTo(IanaLinkRelations.SELF);
assertThat(link.getHref()).isEqualTo("/");
}
@RestController
@RequestMapping("/api")
static class TestController {

View File

@@ -15,20 +15,21 @@
*/
package org.springframework.hateoas.reactive;
import static org.assertj.core.api.AssertionsForInterfaceTypes.*;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Collections;
import java.util.Collection;
import org.assertj.core.api.AssertionsForInterfaceTypes;
import org.junit.Before;
import org.junit.Test;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
@@ -44,7 +45,7 @@ public class ReactiveResourceAssemblerUnitTest {
TestAssemblerWithCustomResources assemblerWithCustomResources;
Employee employee;
ServerWebExchange exchange;
@Before
@@ -59,59 +60,53 @@ public class ReactiveResourceAssemblerUnitTest {
@Test
public void simpleConversionShouldWork() {
this.assembler.toResource(this.employee, this.exchange)
.as(StepVerifier::create)
.expectNextMatches(employeeResource -> {
this.assembler.toResource(this.employee, this.exchange).as(StepVerifier::create)
.expectNextMatches(employeeResource -> {
assertThat(employeeResource.getEmployee()).isEqualTo(new Employee("Frodo Baggins"));
AssertionsForInterfaceTypes.assertThat(employeeResource.getLinks()).containsExactlyInAnyOrder(
new Link("/employees", "employees"));
assertThat(employeeResource.getEmployee()).isEqualTo(new Employee("Frodo Baggins"));
AssertionsForInterfaceTypes.assertThat(employeeResource.getLinks())
.containsExactlyInAnyOrder(new Link("/employees", "employees"));
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
@Test
public void defaultResourcesConversionShouldWork() {
this.assembler.toResources(Flux.just(this.employee), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(employeeResources -> {
this.assembler.toResources(Flux.just(this.employee), this.exchange).as(StepVerifier::create)
.expectNextMatches(employeeResources -> {
assertThat(employeeResources.getContent()).extracting("employee").containsExactly(
new Employee("Frodo Baggins"));
Collection<EmployeeResource> content = employeeResources.getContent();
assertThat(employeeResources.getContent()).extracting("links").containsExactly(
Collections.singletonList(new Link("/employees", "employees")));
assertThat(content) //
.extracting("employee") //
.containsExactly(new Employee("Frodo Baggins"));
assertThat(employeeResources.getLinks()).isEmpty();
assertThat(content.iterator().next().getLinks()).containsExactly(new Link("/employees", "employees"));
assertThat(employeeResources.getLinks()).isEmpty();
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
@Test
public void customResourcesShouldWork() {
this.assemblerWithCustomResources.toResources(Flux.just(this.employee), this.exchange)
.as(StepVerifier::create)
.expectNextMatches(employeeResources -> {
this.assemblerWithCustomResources.toResources(Flux.just(this.employee), this.exchange) //
.as(StepVerifier::create) //
.expectNextMatches(employeeResources -> {
AssertionsForInterfaceTypes.assertThat(employeeResources.getContent()).extracting("employee").containsExactly(
new Employee("Frodo Baggins"));
assertThat(employeeResources.getLinks()) //
.containsExactlyInAnyOrder(new Link("/employees").withSelfRel(), new Link("/", "root"));
AssertionsForInterfaceTypes.assertThat(employeeResources.getContent()).extracting("links").containsExactly(
Collections.singletonList(new Link("/employees", "employees")));
EmployeeResource content = employeeResources.getContent().iterator().next();
AssertionsForInterfaceTypes.assertThat(employeeResources.getLinks()).containsExactlyInAnyOrder(
new Link("/employees").withSelfRel(),
new Link("/", "root")
);
assertThat(content.getEmployee()).isEqualTo(new Employee("Frodo Baggins"));
assertThat(content.getLinks()).containsExactly(new Link("/employees", "employees"));
return true;
})
.verifyComplete();
return true;
}).verifyComplete();
}
class TestAssembler implements ReactiveResourceAssembler<Employee, EmployeeResource> {
@@ -121,7 +116,7 @@ public class ReactiveResourceAssemblerUnitTest {
EmployeeResource employeeResource = new EmployeeResource(entity);
employeeResource.add(new Link("/employees", "employees"));
return Mono.just(employeeResource);
}
}
@@ -129,20 +124,18 @@ public class ReactiveResourceAssemblerUnitTest {
class TestAssemblerWithCustomResources extends TestAssembler {
@Override
public Mono<Resources<EmployeeResource>> toResources(Flux<? extends Employee> entities, ServerWebExchange exchange) {
public Mono<Resources<EmployeeResource>> toResources(Flux<? extends Employee> entities,
ServerWebExchange exchange) {
return entities
.flatMap(entity -> toResource(entity, exchange))
.collectList()
.map(listOfResources -> {
return entities.flatMap(entity -> toResource(entity, exchange)).collectList().map(listOfResources -> {
Resources<EmployeeResource> employeeResources = new Resources<>(listOfResources);
employeeResources.add(new Link("/employees").withSelfRel());
employeeResources.add(new Link("/", "root"));
Resources<EmployeeResource> employeeResources = new Resources<>(listOfResources);
return employeeResources;
});
employeeResources.add(new Link("/employees").withSelfRel());
employeeResources.add(new Link("/", "root"));
return employeeResources;
});
}
}
@@ -153,6 +146,7 @@ public class ReactiveResourceAssemblerUnitTest {
}
@Data
@EqualsAndHashCode(callSuper = true)
@AllArgsConstructor
class EmployeeResource extends ResourceSupport {
private Employee employee;

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2019 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.support;
import org.assertj.core.matcher.AssertionMatcher;
import org.hamcrest.Matcher;
import org.springframework.lang.Nullable;
import org.springframework.test.util.JsonPathExpectationsHelper;
/**
* @author Greg Turnquist
*/
public class JsonPathUtils {
public static <T> AssertionMatcher<String> jsonPath(String expression, Matcher<T> matcher) {
return new AssertionMatcher<String>() {
@Override
public void assertion(String actual) throws AssertionError {
new JsonPathExpectationsHelper(expression).assertValue(actual, matcher);
}
};
}
public static AssertionMatcher<String> jsonPath(String expression, @Nullable Object expectedValue) {
return new AssertionMatcher<String>() {
@Override
public void assertion(String actual) throws AssertionError {
new JsonPathExpectationsHelper(expression).assertValue(actual, expectedValue);
}
};
}
}

View File

@@ -50,4 +50,4 @@ public final class MappingUtils {
return builder.toString();
}
}
}
}

View File

@@ -0,0 +1,171 @@
/*
* Copyright 2019 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.support;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.reactive.ReactiveLinkBuilder.linkTo;
import static reactor.function.TupleUtils.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.reactive.ReactiveLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Greg Turnquist
*/
@RestController
public class WebFluxEmployeeController {
private static Map<Integer, Employee> EMPLOYEES;
public static void reset() {
EMPLOYEES = new TreeMap<>();
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
}
@GetMapping("/employees")
public Mono<Resources<Resource<Employee>>> all() {
return Flux.fromIterable(EMPLOYEES.keySet()).flatMap(id -> findOne(id)).collectList().flatMap(
resources -> linkTo(methodOn(WebFluxEmployeeController.class).all()).map(ReactiveLinkBuilder::withSelfRel)
.map(link -> link.andAffordance(afford(methodOn(WebFluxEmployeeController.class).newEmployee(null)))
.andAffordance(afford(methodOn(WebFluxEmployeeController.class).search(null, null))))
.map(selfLink -> new Resources<>(resources, selfLink)));
}
@GetMapping("/employees/search")
public Mono<Resources<Resource<Employee>>> search(@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "role", required = false) String role) {
return Flux.fromIterable(EMPLOYEES.keySet()).flatMap(id -> findOne(id)).filter(resource -> {
boolean nameMatches = Optional.ofNullable(name).map(s -> resource.getContent().getName().contains(s))
.orElse(true);
boolean roleMatches = Optional.ofNullable(role).map(s -> resource.getContent().getRole().contains(s))
.orElse(true);
return nameMatches && roleMatches;
}).collectList().flatMap(
resources -> linkTo(methodOn(WebFluxEmployeeController.class).all()).map(ReactiveLinkBuilder::withSelfRel)
.map(link -> link.andAffordance(afford(methodOn(WebFluxEmployeeController.class).newEmployee(null)))
.andAffordance(afford(methodOn(WebFluxEmployeeController.class).search(null, null))))
.map(selfLink -> new Resources<>(resources, selfLink)));
}
@GetMapping("/employees/{id}")
public Mono<Resource<Employee>> findOne(@PathVariable Integer id) {
return linkTo(methodOn(WebFluxEmployeeController.class).findOne(id)) //
.map(ReactiveLinkBuilder::withSelfRel) //
.zipWith(linkTo(methodOn(WebFluxEmployeeController.class).all()) //
.map(reactiveLinkBuilder -> reactiveLinkBuilder.withRel("employees")))
.map(function((selfLink, employeesLink) -> new Resource<>(EMPLOYEES.get(id),
selfLink.andAffordance(afford(methodOn(WebFluxEmployeeController.class).updateEmployee(null, id))) //
.andAffordance(afford(methodOn(WebFluxEmployeeController.class).partiallyUpdateEmployee(null, id))),
employeesLink)));
}
@PostMapping("/employees")
public Mono<ResponseEntity<?>> newEmployee(@RequestBody Mono<Resource<Employee>> employee) {
return employee.flatMap(resource -> {
int newEmployeeId = EMPLOYEES.size();
EMPLOYEES.put(newEmployeeId, resource.getContent());
return findOne(newEmployeeId);
}).map(findOne -> {
return ResponseEntity.created(URI.create(findOne //
.getLink(IanaLinkRelations.SELF) //
.map(link -> link.expand().getHref()) //
.orElse(""))) //
.build();
});
}
@PutMapping("/employees/{id}")
public Mono<ResponseEntity<?>> updateEmployee(@RequestBody Mono<Resource<Employee>> employee,
@PathVariable Integer id) {
return employee.flatMap(resource -> {
EMPLOYEES.put(id, resource.getContent());
return findOne(id);
}).map(findOne -> {
return ResponseEntity.noContent() //
.location(URI.create(findOne //
.getLink(IanaLinkRelations.SELF) //
.map(link -> link.expand().getHref()) //
.orElse(""))) //
.build();
});
}
@PatchMapping("/employees/{id}")
public Mono<ResponseEntity<?>> partiallyUpdateEmployee( //
@RequestBody Mono<Resource<Employee>> employee, @PathVariable Integer id) {
return employee.flatMap(resource -> {
Employee oldEmployee = EMPLOYEES.get(id);
Employee newEmployee = oldEmployee;
if (resource.getContent().getName() != null) {
newEmployee = newEmployee.withName(resource.getContent().getName());
}
if (resource.getContent().getRole() != null) {
newEmployee = newEmployee.withRole(resource.getContent().getRole());
}
EMPLOYEES.put(id, newEmployee);
return findOne(id);
}).map(findOne -> {
return ResponseEntity.noContent() //
.location(URI.create(findOne //
.getLink(IanaLinkRelations.SELF) //
.map(link -> link.expand().getHref()) //
.orElse(""))) //
.build();
});
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2019 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.support;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* @author Greg Turnquist
*/
@RestController
public class WebMvcEmployeeController {
private static Map<Integer, Employee> EMPLOYEES;
public static void reset() {
EMPLOYEES = new TreeMap<>();
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
}
@GetMapping("/employees")
public Resources<Resource<Employee>> all() {
// Create a list of Resource<Employee>'s to return
List<Resource<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
employees.add(findOne(i));
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(WebMvcEmployeeController.class).all()).withSelfRel() //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).newEmployee(null))) //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).search(null, null)));
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
}
@GetMapping("/employees/search")
public Resources<Resource<Employee>> search(@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "role", required = false) String role) {
// Create a list of Resource<Employee>'s to return
List<Resource<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
Resource<Employee> employeeResource = findOne(i);
boolean nameMatches = Optional.ofNullable(name) //
.map(s -> employeeResource.getContent().getName().contains(s)) //
.orElse(true);
boolean roleMatches = Optional.ofNullable(role) //
.map(s -> employeeResource.getContent().getRole().contains(s)) //
.orElse(true);
if (nameMatches && roleMatches) {
employees.add(employeeResource);
}
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(WebMvcEmployeeController.class).all()) //
.withSelfRel() //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).newEmployee(null))) //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).search(null, null)));
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
}
@GetMapping("/employees/{id}")
public Resource<Employee> findOne(@PathVariable Integer id) {
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(WebMvcEmployeeController.class).findOne(id)).withSelfRel();
// Define final link as means to find entire collection.
Link employeesLink = linkTo(methodOn(WebMvcEmployeeController.class).all()).withRel("employees");
// Return the affordance + a link back to the entire collection resource.
return new Resource<>(EMPLOYEES.get(id), //
findOneLink //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).updateEmployee(null, id))) // //
.andAffordance(afford(methodOn(WebMvcEmployeeController.class).partiallyUpdateEmployee(null, id))), //
employeesLink);
}
@PostMapping("/employees")
public ResponseEntity<?> newEmployee(@RequestBody Resource<Employee> employee) {
int newEmployeeId = EMPLOYEES.size();
EMPLOYEES.put(newEmployeeId, employee.getContent());
Link link = linkTo(methodOn(getClass()).findOne(newEmployeeId)).withSelfRel().expand();
return ResponseEntity.created(URI.create(link.getHref())).build();
}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee(@RequestBody Resource<Employee> employee, @PathVariable Integer id) {
EMPLOYEES.put(id, employee.getContent());
Link link = linkTo(methodOn(getClass()).findOne(id)).withSelfRel().expand();
return ResponseEntity.noContent() //
.location(URI.create(link.getHref())) //
.build();
}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee(@RequestBody Resource<Employee> employee, @PathVariable Integer id) {
Employee oldEmployee = EMPLOYEES.get(id);
Employee newEmployee = oldEmployee;
if (employee.getContent().getName() != null) {
newEmployee = newEmployee.withName(employee.getContent().getName());
}
if (employee.getContent().getRole() != null) {
newEmployee = newEmployee.withRole(employee.getContent().getRole());
}
EMPLOYEES.put(id, newEmployee);
try {
return ResponseEntity //
.noContent() //
.location( //
new URI(findOne(id) //
.getLink(IanaLinkRelations.SELF) //
.map(link -> link.expand().getHref()) //
.orElse("") //
) //
).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}

View File

@@ -0,0 +1,261 @@
/*
* Copyright 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.uber;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.support.JsonPathUtils.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.config.reactive.WebClientConfigurer;
import org.springframework.hateoas.support.WebFluxEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.config.EnableWebFlux;
/**
* @author Greg Turnquist
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class UberWebFluxIntegrationTest {
@Autowired WebTestClient testClient;
@Before
public void setUp() {
WebFluxEmployeeController.reset();
}
/**
* @see #728
*/
@Test
public void singleEmployee() {
this.testClient.get().uri("http://localhost/employees/0")
.accept(MediaTypes.UBER_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.UBER_JSON)
.expectBody(String.class)
.value(jsonPath("$.uber.version", is("1.0")))
.value(jsonPath("$.uber.data.*", hasSize(5))) //
.value(jsonPath("$.uber.data[0].name", is("self"))) //
.value(jsonPath("$.uber.data[0].rel[0]", is("self"))) //
.value(jsonPath("$.uber.data[0].rel[1]", is("findOne"))) //
.value(jsonPath("$.uber.data[0].url", is("http://localhost/employees/0")))
.value(jsonPath("$.uber.data[1].name", is("updateEmployee")))
.value(jsonPath("$.uber.data[1].rel[0]", is("updateEmployee")))
.value(jsonPath("$.uber.data[1].url", is("http://localhost/employees/0")))
.value(jsonPath("$.uber.data[1].action", is("replace")))
.value(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[2].name", is("partiallyUpdateEmployee")))
.value(jsonPath("$.uber.data[2].rel[0]", is("partiallyUpdateEmployee")))
.value(jsonPath("$.uber.data[2].url", is("http://localhost/employees/0")))
.value(jsonPath("$.uber.data[2].action", is("partial")))
.value(jsonPath("$.uber.data[2].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[3].name", is("employees")))
.value(jsonPath("$.uber.data[3].rel[0]", is("employees")))
.value(jsonPath("$.uber.data[3].rel[1]", is("all")))
.value(jsonPath("$.uber.data[3].url", is("http://localhost/employees")))
.value(jsonPath("$.uber.data[4].name", is("employee")))
.value(jsonPath("$.uber.data[4].data.*", hasSize(2)))
.value(jsonPath("$.uber.data[4].data[0].name", is("role")))
.value(jsonPath("$.uber.data[4].data[0].value", is("ring bearer")))
.value(jsonPath("$.uber.data[4].data[1].name", is("name")))
.value(jsonPath("$.uber.data[4].data[1].value", is("Frodo Baggins")));
}
/**
* @see #728
*/
@Test
public void collectionOfEmployees() {
this.testClient.get().uri("http://localhost/employees")
.accept(MediaTypes.UBER_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.UBER_JSON)
.expectBody(String.class)
.value(jsonPath("$.uber.version", is("1.0")))
.value(jsonPath("$.uber.data.*", hasSize(4)))
.value(jsonPath("$.uber.data[0].name", is("self"))) //
.value(jsonPath("$.uber.data[0].rel[0]", is("self")))
.value(jsonPath("$.uber.data[0].rel[1]", is("all")))
.value(jsonPath("$.uber.data[0].url", is("http://localhost/employees")))
.value(jsonPath("$.uber.data[1].name", is("newEmployee")))
.value(jsonPath("$.uber.data[1].rel[0]", is("newEmployee")))
.value(jsonPath("$.uber.data[1].url", is("http://localhost/employees")))
.value(jsonPath("$.uber.data[1].action", is("append")))
.value(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[2].data[0].name", is("self")))
.value(jsonPath("$.uber.data[2].data[0].rel[0]", is("self")))
.value(jsonPath("$.uber.data[2].data[0].rel[1]", is("findOne")))
.value(jsonPath("$.uber.data[2].data[0].url", is("http://localhost/employees/0")))
.value(jsonPath("$.uber.data[2].data[1].name", is("updateEmployee")))
.value(jsonPath("$.uber.data[2].data[1].rel[0]", is("updateEmployee")))
.value(jsonPath("$.uber.data[2].data[1].url", is("http://localhost/employees/0")))
.value(jsonPath("$.uber.data[2].data[1].action", is("replace")))
.value(jsonPath("$.uber.data[2].data[1].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[2].data[2].name", is("partiallyUpdateEmployee")))
.value(jsonPath("$.uber.data[2].data[2].rel[0]", is("partiallyUpdateEmployee")))
.value(jsonPath("$.uber.data[2].data[2].url", is("http://localhost/employees/0")))
.value(jsonPath("$.uber.data[2].data[2].action", is("partial")))
.value(jsonPath("$.uber.data[2].data[2].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[2].data[3].rel[0]", is("employees")))
.value(jsonPath("$.uber.data[2].data[3].rel[1]", is("all")))
.value(jsonPath("$.uber.data[2].data[3].url", is("http://localhost/employees")))
.value(jsonPath("$.uber.data[2].data[4].name", is("employee")))
.value(jsonPath("$.uber.data[2].data[4].data[0].name", is("role")))
.value(jsonPath("$.uber.data[2].data[4].data[0].value", is("ring bearer")))
.value(jsonPath("$.uber.data[2].data[4].data[1].name", is("name")))
.value(jsonPath("$.uber.data[2].data[4].data[1].value", is("Frodo Baggins")))
.value(jsonPath("$.uber.data[3].data[0].name", is("self")))
.value(jsonPath("$.uber.data[3].data[0].rel[0]", is("self")))
.value(jsonPath("$.uber.data[3].data[0].rel[1]", is("findOne")))
.value(jsonPath("$.uber.data[3].data[0].url", is("http://localhost/employees/1")))
.value(jsonPath("$.uber.data[3].data[1].name", is("updateEmployee")))
.value(jsonPath("$.uber.data[3].data[1].rel[0]", is("updateEmployee")))
.value(jsonPath("$.uber.data[3].data[1].url", is("http://localhost/employees/1")))
.value(jsonPath("$.uber.data[3].data[1].action", is("replace")))
.value(jsonPath("$.uber.data[3].data[1].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[3].data[2].name", is("partiallyUpdateEmployee")))
.value(jsonPath("$.uber.data[3].data[2].rel[0]", is("partiallyUpdateEmployee")))
.value(jsonPath("$.uber.data[3].data[2].url", is("http://localhost/employees/1")))
.value(jsonPath("$.uber.data[3].data[2].action", is("partial")))
.value(jsonPath("$.uber.data[3].data[2].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[3].data[3].rel[0]", is("employees")))
.value(jsonPath("$.uber.data[3].data[3].rel[1]", is("all")))
.value(jsonPath("$.uber.data[3].data[3].url", is("http://localhost/employees")))
.value(jsonPath("$.uber.data[3].data[4].name", is("employee")))
.value(jsonPath("$.uber.data[3].data[4].data[0].name", is("role")))
.value(jsonPath("$.uber.data[3].data[4].data[0].value", is("burglar")))
.value(jsonPath("$.uber.data[3].data[4].data[1].name", is("name")))
.value(jsonPath("$.uber.data[3].data[4].data[1].value", is("Bilbo Baggins")));
}
/**
* @see #728
*/
@Test
public void createNewEmployee() throws Exception {
String input = read(new ClassPathResource("create-employee.json", getClass()));
this.testClient.post().uri("http://localhost/employees")
.contentType(MediaTypes.UBER_JSON)
.syncBody(input)
.exchange()
.expectStatus().isCreated()
.expectHeader().valueEquals(HttpHeaders.LOCATION, "http://localhost/employees/2");
this.testClient.get().uri("http://localhost/employees/2")
.accept(MediaTypes.UBER_JSON)
.exchange()
.expectStatus().isOk()
.expectHeader().contentType(MediaTypes.UBER_JSON)
.expectBody(String.class)
.value(jsonPath("$.uber.version", is("1.0")))
.value(jsonPath("$.uber.data.*", hasSize(5))) //
.value(jsonPath("$.uber.data[0].name", is("self"))) //
.value(jsonPath("$.uber.data[0].rel[0]", is("self"))) //
.value(jsonPath("$.uber.data[0].rel[1]", is("findOne"))) //
.value(jsonPath("$.uber.data[0].url", is("http://localhost/employees/2")))
.value(jsonPath("$.uber.data[1].name", is("updateEmployee"))) //
.value(jsonPath("$.uber.data[1].rel[0]", is("updateEmployee"))) //
.value(jsonPath("$.uber.data[1].url", is("http://localhost/employees/2"))) //
.value(jsonPath("$.uber.data[1].action", is("replace"))) //
.value(jsonPath("$.uber.data[1].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[2].name", is("partiallyUpdateEmployee"))) //
.value(jsonPath("$.uber.data[2].rel[0]", is("partiallyUpdateEmployee"))) //
.value(jsonPath("$.uber.data[2].url", is("http://localhost/employees/2"))) //
.value(jsonPath("$.uber.data[2].action", is("partial"))) //
.value(jsonPath("$.uber.data[2].model", is("name={name}&role={role}")))
.value(jsonPath("$.uber.data[3].name", is("employees"))) //
.value(jsonPath("$.uber.data[3].rel[0]", is("employees"))) //
.value(jsonPath("$.uber.data[3].rel[1]", is("all"))) //
.value(jsonPath("$.uber.data[3].url", is("http://localhost/employees")))
.value(jsonPath("$.uber.data[4].name", is("employee"))) //
.value(jsonPath("$.uber.data[4].data.*", hasSize(2))) //
.value(jsonPath("$.uber.data[4].data[0].name", is("role"))) //
.value(jsonPath("$.uber.data[4].data[0].value", is("gardener"))) //
.value(jsonPath("$.uber.data[4].data[1].name", is("name"))) //
.value(jsonPath("$.uber.data[4].data[1].value", is("Samwise Gamgee")));
}
@Configuration
@EnableWebFlux
@EnableHypermediaSupport(type = { HypermediaType.UBER })
static class TestConfig {
@Bean
WebFluxEmployeeController employeeController() {
return new WebFluxEmployeeController();
}
@Bean
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
return WebTestClient.bindToApplicationContext(ctx).build()
.mutate()
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies())
.build();
}
}
}

View File

@@ -17,18 +17,12 @@ package org.springframework.hateoas.uber;
import static org.hamcrest.CoreMatchers.*;
import static org.hamcrest.collection.IsCollectionWithSize.*;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import static org.springframework.hateoas.support.MappingUtils.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;
import org.junit.Before;
@@ -41,25 +35,15 @@ import org.springframework.core.io.ClassPathResource;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.support.Employee;
import org.springframework.hateoas.support.WebMvcEmployeeController;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@@ -75,17 +59,11 @@ public class UberWebMvcIntegrationTest {
MockMvc mockMvc;
private static Map<Integer, Employee> EMPLOYEES;
@Before
public void setUp() {
this.mockMvc = webAppContextSetup(this.context).build();
EMPLOYEES = new TreeMap<>();
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
WebMvcEmployeeController.reset();
}
/**
@@ -259,162 +237,14 @@ public class UberWebMvcIntegrationTest {
.andExpect(jsonPath("$.uber.data[4].data[1].value", is("Samwise Gamgee")));
}
@RestController
static class EmployeeController {
@GetMapping("/employees")
public Resources<Resource<Employee>> all() {
// Create a list of Resource<Employee>'s to return
List<Resource<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
employees.add(findOne(i));
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(EmployeeController.class).all()).withSelfRel() //
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))) //
.andAffordance(afford(methodOn(EmployeeController.class).search(null, null)));
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
}
@GetMapping("/employees/search")
public Resources<Resource<Employee>> search(@RequestParam(value = "name", required = false) String name,
@RequestParam(value = "role", required = false) String role) {
// Create a list of Resource<Employee>'s to return
List<Resource<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
Resource<Employee> employeeResource = findOne(i);
boolean nameMatches = Optional.ofNullable(name) //
.map(s -> employeeResource.getContent().getName().contains(s)) //
.orElse(true);
boolean roleMatches = Optional.ofNullable(role) //
.map(s -> employeeResource.getContent().getRole().contains(s)) //
.orElse(true);
if (nameMatches && roleMatches) {
employees.add(employeeResource);
}
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(EmployeeController.class).all()) //
.withSelfRel() //
.andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))) //
.andAffordance(afford(methodOn(EmployeeController.class).search(null, null)));
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
}
@GetMapping("/employees/{id}")
public Resource<Employee> findOne(@PathVariable Integer id) {
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
// Define final link as means to find entire collection.
Link employeesLink = linkTo(methodOn(EmployeeController.class).all()).withRel("employees");
// Return the affordance + a link back to the entire collection resource.
return new Resource<>(EMPLOYEES.get(id), //
findOneLink //
.andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, id))) // //
.andAffordance(afford(methodOn(EmployeeController.class).partiallyUpdateEmployee(null, id))), //
employeesLink);
}
@PostMapping("/employees")
public ResponseEntity<?> newEmployee(@RequestBody Resource<Employee> employee) {
int newEmployeeId = EMPLOYEES.size();
EMPLOYEES.put(newEmployeeId, employee.getContent());
try {
return ResponseEntity.created( //
new URI(findOne(newEmployeeId) //
.getLink(IanaLinkRelations.SELF.value()) //
.map(link -> link.expand().getHref()) //
.orElse("") //
) //
).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee(@RequestBody Resource<Employee> employee, @PathVariable Integer id) {
EMPLOYEES.put(id, employee.getContent());
try {
return ResponseEntity //
.noContent() //
.location( //
new URI(findOne(id) //
.getLink(IanaLinkRelations.SELF.value()) //
.map(link -> link.expand().getHref()) //
.orElse("") //
) //
).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee(@RequestBody Resource<Employee> employee,
@PathVariable Integer id) {
Employee oldEmployee = EMPLOYEES.get(id);
Employee newEmployee = oldEmployee;
if (employee.getContent().getName() != null) {
newEmployee = newEmployee.withName(employee.getContent().getName());
}
if (employee.getContent().getRole() != null) {
newEmployee = newEmployee.withRole(employee.getContent().getRole());
}
EMPLOYEES.put(id, newEmployee);
try {
return ResponseEntity //
.noContent() //
.location( //
new URI(findOne(id) //
.getLink(IanaLinkRelations.SELF.value()) //
.map(link -> link.expand().getHref()) //
.orElse("") //
) //
).build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type = { HypermediaType.UBER })
static class TestConfig {
@Bean
EmployeeController employeeController() {
return new EmployeeController();
WebMvcEmployeeController employeeController() {
return new WebMvcEmployeeController();
}
}
}

View File

@@ -0,0 +1 @@
{"collection":{"version":"1.0","href":"http://localhost/employees/0","links":[{"rel":"employees","href":"http://localhost/employees"}],"items":[{"href":"http://localhost/employees/0","data":[{"name":"role","value":"ring bearer"},{"name":"name","value":"Frodo Baggins"}],"links":[{"rel":"employees","href":"http://localhost/employees"}]}],"template":{"data":[{"name":"name","value":""},{"name":"role","value":""}]}}}