#2035 - Introduce MediaTypeConfigurationCustomizer.
HAL and HAL Forms now support customization of the media type-specific configuration via MediaTypeConfigurationCustomizer instances registered in the application context.
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2023 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
|
||||
*
|
||||
* https://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.mediatype;
|
||||
|
||||
/**
|
||||
* Callback interface to customize media type-specific configuration. Declare instances of the interface as bean methods
|
||||
* in Spring configuration.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 2.2
|
||||
*/
|
||||
public interface MediaTypeConfigurationCustomizer<T> {
|
||||
|
||||
/**
|
||||
* Customize the given configuration instance.
|
||||
*
|
||||
* @param configuration will never be {@literal null}.
|
||||
* @return must not be {@literal null}.
|
||||
*/
|
||||
T customize(T configuration);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2023 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
|
||||
*
|
||||
* https://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.mediatype;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Factory to provide instances of media type-specific configuration processed by
|
||||
* {@link MediaTypeConfigurationCustomizer}s.
|
||||
*
|
||||
* @author Oliver Drotbohm
|
||||
* @since 2.2
|
||||
*/
|
||||
public class MediaTypeConfigurationFactory<T, S extends MediaTypeConfigurationCustomizer<T>> {
|
||||
|
||||
private final Supplier<T> supplier;
|
||||
private final Supplier<Stream<S>> customizers;
|
||||
|
||||
private T resolved;
|
||||
|
||||
/**
|
||||
* Creates a new {@link MediaTypeConfigurationFactory} for the given supplier of the original instance and all
|
||||
* {@link MediaTypeConfigurationCustomizer}s.
|
||||
*
|
||||
* @param supplier must not be {@literal null}.
|
||||
* @param customizers must not be {@literal null}.
|
||||
*/
|
||||
MediaTypeConfigurationFactory(Supplier<T> supplier, Supplier<Stream<S>> customizers) {
|
||||
|
||||
Assert.notNull(supplier, "Supplier must not be null!");
|
||||
Assert.notNull(customizers, "Customizers must not be null!");
|
||||
|
||||
this.supplier = supplier;
|
||||
this.customizers = customizers;
|
||||
}
|
||||
|
||||
public MediaTypeConfigurationFactory(Supplier<T> supplier, ObjectProvider<S> customizers) {
|
||||
this(supplier, () -> customizers.orderedStream());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the customized configuration instance.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
public T getConfiguration() {
|
||||
|
||||
if (resolved == null) {
|
||||
|
||||
var source = supplier.get();
|
||||
|
||||
Assert.notNull(source, "Source instance must not be null!");
|
||||
|
||||
this.resolved = this.customizers.get()
|
||||
.reduce(source, (config, customizer) -> customizer.customize(config), (__, r) -> r);
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.hateoas.client.LinkDiscoverer;
|
||||
import org.springframework.hateoas.config.HypermediaMappingInformation;
|
||||
import org.springframework.hateoas.mediatype.MediaTypeConfigurationCustomizer;
|
||||
import org.springframework.hateoas.mediatype.MediaTypeConfigurationFactory;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.hateoas.server.LinkRelationProvider;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -42,19 +44,19 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
|
||||
private final LinkRelationProvider relProvider;
|
||||
private final ObjectProvider<CurieProvider> curieProvider;
|
||||
private final ObjectProvider<HalConfiguration> halConfiguration;
|
||||
private final MediaTypeConfigurationFactory<HalConfiguration, ? extends MediaTypeConfigurationCustomizer<HalConfiguration>> configurationFactory;
|
||||
private final @Qualifier("messageResolver") MessageResolver resolver;
|
||||
private final AutowireCapableBeanFactory beanFactory;
|
||||
|
||||
private HalConfiguration resolvedConfiguration;
|
||||
|
||||
public HalMediaTypeConfiguration(LinkRelationProvider relProvider, ObjectProvider<CurieProvider> curieProvider,
|
||||
ObjectProvider<HalConfiguration> halConfiguration, MessageResolver resolver,
|
||||
AutowireCapableBeanFactory beanFactory) {
|
||||
ObjectProvider<HalConfiguration> halConfiguration,
|
||||
ObjectProvider<MediaTypeConfigurationCustomizer<HalConfiguration>> customizers,
|
||||
MessageResolver resolver, AutowireCapableBeanFactory beanFactory) {
|
||||
|
||||
this.relProvider = relProvider;
|
||||
this.curieProvider = curieProvider;
|
||||
this.halConfiguration = halConfiguration;
|
||||
this.configurationFactory = new MediaTypeConfigurationFactory<>(
|
||||
() -> halConfiguration.getIfAvailable(HalConfiguration::new), customizers);
|
||||
this.resolver = resolver;
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
@@ -70,7 +72,7 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
*/
|
||||
@Override
|
||||
public List<MediaType> getMediaTypes() {
|
||||
return getResolvedConfiguration().getMediaTypes();
|
||||
return configurationFactory.getConfiguration().getMediaTypes();
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -80,7 +82,7 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
@Override
|
||||
public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
|
||||
|
||||
HalConfiguration halConfiguration = getResolvedConfiguration();
|
||||
HalConfiguration halConfiguration = configurationFactory.getConfiguration();
|
||||
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
mapper.registerModule(new Jackson2HalModule());
|
||||
@@ -91,18 +93,4 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
|
||||
return mapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lookup and cache the {@link HalConfiguration} instance to be used.
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
private HalConfiguration getResolvedConfiguration() {
|
||||
|
||||
if (resolvedConfiguration == null) {
|
||||
this.resolvedConfiguration = halConfiguration.getIfAvailable(HalConfiguration::new);
|
||||
}
|
||||
|
||||
return resolvedConfiguration;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.hateoas.client.LinkDiscoverer;
|
||||
import org.springframework.hateoas.config.HypermediaMappingInformation;
|
||||
import org.springframework.hateoas.mediatype.MediaTypeConfigurationCustomizer;
|
||||
import org.springframework.hateoas.mediatype.MediaTypeConfigurationFactory;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.hateoas.mediatype.hal.CurieProvider;
|
||||
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
|
||||
@@ -45,22 +47,32 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
|
||||
private final DelegatingLinkRelationProvider relProvider;
|
||||
private final ObjectProvider<CurieProvider> curieProvider;
|
||||
private final ObjectProvider<HalFormsConfiguration> halFormsConfiguration;
|
||||
private final ObjectProvider<HalConfiguration> halConfiguration;
|
||||
private final MediaTypeConfigurationFactory<HalFormsConfiguration, ? extends MediaTypeConfigurationCustomizer<HalFormsConfiguration>> configurationFactory;
|
||||
private final MessageResolver resolver;
|
||||
private final AbstractAutowireCapableBeanFactory beanFactory;
|
||||
|
||||
private HalFormsConfiguration resolvedConfiguration;
|
||||
|
||||
public HalFormsMediaTypeConfiguration(DelegatingLinkRelationProvider relProvider,
|
||||
ObjectProvider<CurieProvider> curieProvider, ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
|
||||
ObjectProvider<HalConfiguration> halConfiguration, MessageResolver resolver,
|
||||
AbstractAutowireCapableBeanFactory beanFactory) {
|
||||
ObjectProvider<CurieProvider> curieProvider,
|
||||
ObjectProvider<HalConfiguration> halConfiguration,
|
||||
ObjectProvider<MediaTypeConfigurationCustomizer<HalConfiguration>> halCustomizers,
|
||||
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
|
||||
ObjectProvider<MediaTypeConfigurationCustomizer<HalFormsConfiguration>> halFormsCustomizers,
|
||||
MessageResolver resolver, AbstractAutowireCapableBeanFactory beanFactory) {
|
||||
|
||||
this.relProvider = relProvider;
|
||||
this.curieProvider = curieProvider;
|
||||
this.halFormsConfiguration = halFormsConfiguration;
|
||||
this.halConfiguration = halConfiguration;
|
||||
|
||||
Supplier<HalFormsConfiguration> defaultConfig = () -> {
|
||||
|
||||
MediaTypeConfigurationFactory<HalConfiguration, ?> customizedHalConfiguration = new MediaTypeConfigurationFactory<>(
|
||||
() -> halConfiguration.getIfAvailable(HalConfiguration::new), halCustomizers);
|
||||
|
||||
return new HalFormsConfiguration(
|
||||
customizedHalConfiguration.getConfiguration());
|
||||
};
|
||||
|
||||
this.configurationFactory = new MediaTypeConfigurationFactory<>(
|
||||
() -> halFormsConfiguration.getIfAvailable(defaultConfig), halFormsCustomizers);
|
||||
this.resolver = resolver;
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
@@ -73,7 +85,7 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
@Bean
|
||||
HalFormsTemplatePropertyWriter halFormsTemplatePropertyWriter() {
|
||||
|
||||
HalFormsConfiguration configuration = getResolvedConfiguration();
|
||||
HalFormsConfiguration configuration = configurationFactory.getConfiguration();
|
||||
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, resolver);
|
||||
|
||||
return new HalFormsTemplatePropertyWriter(builder);
|
||||
@@ -86,7 +98,7 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
@Override
|
||||
public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
|
||||
|
||||
HalFormsConfiguration halFormsConfig = getResolvedConfiguration();
|
||||
HalFormsConfiguration halFormsConfig = configurationFactory.getConfiguration();
|
||||
CurieProvider provider = curieProvider.getIfAvailable(() -> CurieProvider.NONE);
|
||||
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
@@ -105,18 +117,15 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
|
||||
*/
|
||||
@Override
|
||||
public List<MediaType> getMediaTypes() {
|
||||
return getResolvedConfiguration().getMediaTypes();
|
||||
return configurationFactory.getConfiguration().getMediaTypes();
|
||||
}
|
||||
|
||||
/**
|
||||
* For testing purposes.
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
HalFormsConfiguration getResolvedConfiguration() {
|
||||
|
||||
Supplier<HalFormsConfiguration> defaultConfig = () -> new HalFormsConfiguration(
|
||||
halConfiguration.getIfAvailable(HalConfiguration::new));
|
||||
|
||||
if (resolvedConfiguration == null) {
|
||||
this.resolvedConfiguration = halFormsConfiguration.getIfAvailable(defaultConfig);
|
||||
}
|
||||
|
||||
return resolvedConfiguration;
|
||||
return configurationFactory.getConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2023 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
|
||||
*
|
||||
* https://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.mediatype;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class MediaTypeConfigurationFactoryUnitTests {
|
||||
|
||||
@Mock MediaTypeConfigurationCustomizer<Object> first, second;
|
||||
|
||||
@Test // GH-2035
|
||||
void invokesCustomizers() {
|
||||
|
||||
var source = new Object();
|
||||
var afterFirst = new Object();
|
||||
var afterSecond = new Object();
|
||||
|
||||
doReturn(afterFirst).when(first).customize(source);
|
||||
doReturn(afterSecond).when(second).customize(afterFirst);
|
||||
|
||||
var factory = new MediaTypeConfigurationFactory<>(() -> source, () -> Stream.of(first, second));
|
||||
|
||||
assertThat(factory.getConfiguration()).isSameAs(afterSecond);
|
||||
|
||||
verify(first, times(1)).customize(source);
|
||||
verify(second, times(1)).customize(afterFirst);
|
||||
|
||||
assertThat(factory.getConfiguration()).isSameAs(afterSecond);
|
||||
|
||||
// Does not re-process source instance
|
||||
verify(first, times(1)).customize(any());
|
||||
verify(second, times(1)).customize(any());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user