#728 - Polishing.
Adapt to API changes in master branch. Fixed formatting.
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,4 @@ public final class MappingUtils {
|
||||
return builder.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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":""}]}}}
|
||||
Reference in New Issue
Block a user