#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:
Oliver Drotbohm
2023-10-16 08:25:13 +02:00
parent f3a1177b82
commit a796509532
5 changed files with 211 additions and 43 deletions

View File

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

View File

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

View File

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

View File

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

View File

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