Skip to content

Commit a796509

Browse files
committed
#2035 - Introduce MediaTypeConfigurationCustomizer.
HAL and HAL Forms now support customization of the media type-specific configuration via MediaTypeConfigurationCustomizer instances registered in the application context.
1 parent f3a1177 commit a796509

File tree

5 files changed

+211
-43
lines changed

5 files changed

+211
-43
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype;
17+
18+
/**
19+
* Callback interface to customize media type-specific configuration. Declare instances of the interface as bean methods
20+
* in Spring configuration.
21+
*
22+
* @author Oliver Drotbohm
23+
* @since 2.2
24+
*/
25+
public interface MediaTypeConfigurationCustomizer<T> {
26+
27+
/**
28+
* Customize the given configuration instance.
29+
*
30+
* @param configuration will never be {@literal null}.
31+
* @return must not be {@literal null}.
32+
*/
33+
T customize(T configuration);
34+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype;
17+
18+
import java.util.function.Supplier;
19+
import java.util.stream.Stream;
20+
21+
import org.springframework.beans.factory.ObjectProvider;
22+
import org.springframework.util.Assert;
23+
24+
/**
25+
* Factory to provide instances of media type-specific configuration processed by
26+
* {@link MediaTypeConfigurationCustomizer}s.
27+
*
28+
* @author Oliver Drotbohm
29+
* @since 2.2
30+
*/
31+
public class MediaTypeConfigurationFactory<T, S extends MediaTypeConfigurationCustomizer<T>> {
32+
33+
private final Supplier<T> supplier;
34+
private final Supplier<Stream<S>> customizers;
35+
36+
private T resolved;
37+
38+
/**
39+
* Creates a new {@link MediaTypeConfigurationFactory} for the given supplier of the original instance and all
40+
* {@link MediaTypeConfigurationCustomizer}s.
41+
*
42+
* @param supplier must not be {@literal null}.
43+
* @param customizers must not be {@literal null}.
44+
*/
45+
MediaTypeConfigurationFactory(Supplier<T> supplier, Supplier<Stream<S>> customizers) {
46+
47+
Assert.notNull(supplier, "Supplier must not be null!");
48+
Assert.notNull(customizers, "Customizers must not be null!");
49+
50+
this.supplier = supplier;
51+
this.customizers = customizers;
52+
}
53+
54+
public MediaTypeConfigurationFactory(Supplier<T> supplier, ObjectProvider<S> customizers) {
55+
this(supplier, () -> customizers.orderedStream());
56+
}
57+
58+
/**
59+
* Returns the customized configuration instance.
60+
*
61+
* @return will never be {@literal null}.
62+
*/
63+
public T getConfiguration() {
64+
65+
if (resolved == null) {
66+
67+
var source = supplier.get();
68+
69+
Assert.notNull(source, "Source instance must not be null!");
70+
71+
this.resolved = this.customizers.get()
72+
.reduce(source, (config, customizer) -> customizer.customize(config), (__, r) -> r);
73+
}
74+
75+
return resolved;
76+
}
77+
}

src/main/java/org/springframework/hateoas/mediatype/hal/HalMediaTypeConfiguration.java

+10-22
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.springframework.context.annotation.Configuration;
2525
import org.springframework.hateoas.client.LinkDiscoverer;
2626
import org.springframework.hateoas.config.HypermediaMappingInformation;
27+
import org.springframework.hateoas.mediatype.MediaTypeConfigurationCustomizer;
28+
import org.springframework.hateoas.mediatype.MediaTypeConfigurationFactory;
2729
import org.springframework.hateoas.mediatype.MessageResolver;
2830
import org.springframework.hateoas.server.LinkRelationProvider;
2931
import org.springframework.http.MediaType;
@@ -42,19 +44,19 @@ public class HalMediaTypeConfiguration implements HypermediaMappingInformation {
4244

4345
private final LinkRelationProvider relProvider;
4446
private final ObjectProvider<CurieProvider> curieProvider;
45-
private final ObjectProvider<HalConfiguration> halConfiguration;
47+
private final MediaTypeConfigurationFactory<HalConfiguration, ? extends MediaTypeConfigurationCustomizer<HalConfiguration>> configurationFactory;
4648
private final @Qualifier("messageResolver") MessageResolver resolver;
4749
private final AutowireCapableBeanFactory beanFactory;
4850

49-
private HalConfiguration resolvedConfiguration;
50-
5151
public HalMediaTypeConfiguration(LinkRelationProvider relProvider, ObjectProvider<CurieProvider> curieProvider,
52-
ObjectProvider<HalConfiguration> halConfiguration, MessageResolver resolver,
53-
AutowireCapableBeanFactory beanFactory) {
52+
ObjectProvider<HalConfiguration> halConfiguration,
53+
ObjectProvider<MediaTypeConfigurationCustomizer<HalConfiguration>> customizers,
54+
MessageResolver resolver, AutowireCapableBeanFactory beanFactory) {
5455

5556
this.relProvider = relProvider;
5657
this.curieProvider = curieProvider;
57-
this.halConfiguration = halConfiguration;
58+
this.configurationFactory = new MediaTypeConfigurationFactory<>(
59+
() -> halConfiguration.getIfAvailable(HalConfiguration::new), customizers);
5860
this.resolver = resolver;
5961
this.beanFactory = beanFactory;
6062
}
@@ -70,7 +72,7 @@ LinkDiscoverer halLinkDisocoverer() {
7072
*/
7173
@Override
7274
public List<MediaType> getMediaTypes() {
73-
return getResolvedConfiguration().getMediaTypes();
75+
return configurationFactory.getConfiguration().getMediaTypes();
7476
}
7577

7678
/*
@@ -80,7 +82,7 @@ public List<MediaType> getMediaTypes() {
8082
@Override
8183
public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
8284

83-
HalConfiguration halConfiguration = getResolvedConfiguration();
85+
HalConfiguration halConfiguration = configurationFactory.getConfiguration();
8486

8587
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
8688
mapper.registerModule(new Jackson2HalModule());
@@ -91,18 +93,4 @@ public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
9193

9294
return mapper;
9395
}
94-
95-
/**
96-
* Lookup and cache the {@link HalConfiguration} instance to be used.
97-
*
98-
* @return will never be {@literal null}.
99-
*/
100-
private HalConfiguration getResolvedConfiguration() {
101-
102-
if (resolvedConfiguration == null) {
103-
this.resolvedConfiguration = halConfiguration.getIfAvailable(HalConfiguration::new);
104-
}
105-
106-
return resolvedConfiguration;
107-
}
10896
}

src/main/java/org/springframework/hateoas/mediatype/hal/forms/HalFormsMediaTypeConfiguration.java

+30-21
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.springframework.context.annotation.Configuration;
2525
import org.springframework.hateoas.client.LinkDiscoverer;
2626
import org.springframework.hateoas.config.HypermediaMappingInformation;
27+
import org.springframework.hateoas.mediatype.MediaTypeConfigurationCustomizer;
28+
import org.springframework.hateoas.mediatype.MediaTypeConfigurationFactory;
2729
import org.springframework.hateoas.mediatype.MessageResolver;
2830
import org.springframework.hateoas.mediatype.hal.CurieProvider;
2931
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
@@ -45,22 +47,32 @@ class HalFormsMediaTypeConfiguration implements HypermediaMappingInformation {
4547

4648
private final DelegatingLinkRelationProvider relProvider;
4749
private final ObjectProvider<CurieProvider> curieProvider;
48-
private final ObjectProvider<HalFormsConfiguration> halFormsConfiguration;
49-
private final ObjectProvider<HalConfiguration> halConfiguration;
50+
private final MediaTypeConfigurationFactory<HalFormsConfiguration, ? extends MediaTypeConfigurationCustomizer<HalFormsConfiguration>> configurationFactory;
5051
private final MessageResolver resolver;
5152
private final AbstractAutowireCapableBeanFactory beanFactory;
5253

53-
private HalFormsConfiguration resolvedConfiguration;
54-
5554
public HalFormsMediaTypeConfiguration(DelegatingLinkRelationProvider relProvider,
56-
ObjectProvider<CurieProvider> curieProvider, ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
57-
ObjectProvider<HalConfiguration> halConfiguration, MessageResolver resolver,
58-
AbstractAutowireCapableBeanFactory beanFactory) {
55+
ObjectProvider<CurieProvider> curieProvider,
56+
ObjectProvider<HalConfiguration> halConfiguration,
57+
ObjectProvider<MediaTypeConfigurationCustomizer<HalConfiguration>> halCustomizers,
58+
ObjectProvider<HalFormsConfiguration> halFormsConfiguration,
59+
ObjectProvider<MediaTypeConfigurationCustomizer<HalFormsConfiguration>> halFormsCustomizers,
60+
MessageResolver resolver, AbstractAutowireCapableBeanFactory beanFactory) {
5961

6062
this.relProvider = relProvider;
6163
this.curieProvider = curieProvider;
62-
this.halFormsConfiguration = halFormsConfiguration;
63-
this.halConfiguration = halConfiguration;
64+
65+
Supplier<HalFormsConfiguration> defaultConfig = () -> {
66+
67+
MediaTypeConfigurationFactory<HalConfiguration, ?> customizedHalConfiguration = new MediaTypeConfigurationFactory<>(
68+
() -> halConfiguration.getIfAvailable(HalConfiguration::new), halCustomizers);
69+
70+
return new HalFormsConfiguration(
71+
customizedHalConfiguration.getConfiguration());
72+
};
73+
74+
this.configurationFactory = new MediaTypeConfigurationFactory<>(
75+
() -> halFormsConfiguration.getIfAvailable(defaultConfig), halFormsCustomizers);
6476
this.resolver = resolver;
6577
this.beanFactory = beanFactory;
6678
}
@@ -73,7 +85,7 @@ LinkDiscoverer halFormsLinkDiscoverer() {
7385
@Bean
7486
HalFormsTemplatePropertyWriter halFormsTemplatePropertyWriter() {
7587

76-
HalFormsConfiguration configuration = getResolvedConfiguration();
88+
HalFormsConfiguration configuration = configurationFactory.getConfiguration();
7789
HalFormsTemplateBuilder builder = new HalFormsTemplateBuilder(configuration, resolver);
7890

7991
return new HalFormsTemplatePropertyWriter(builder);
@@ -86,7 +98,7 @@ HalFormsTemplatePropertyWriter halFormsTemplatePropertyWriter() {
8698
@Override
8799
public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
88100

89-
HalFormsConfiguration halFormsConfig = getResolvedConfiguration();
101+
HalFormsConfiguration halFormsConfig = configurationFactory.getConfiguration();
90102
CurieProvider provider = curieProvider.getIfAvailable(() -> CurieProvider.NONE);
91103

92104
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
@@ -105,18 +117,15 @@ public ObjectMapper configureObjectMapper(ObjectMapper mapper) {
105117
*/
106118
@Override
107119
public List<MediaType> getMediaTypes() {
108-
return getResolvedConfiguration().getMediaTypes();
120+
return configurationFactory.getConfiguration().getMediaTypes();
109121
}
110122

123+
/**
124+
* For testing purposes.
125+
*
126+
* @return
127+
*/
111128
HalFormsConfiguration getResolvedConfiguration() {
112-
113-
Supplier<HalFormsConfiguration> defaultConfig = () -> new HalFormsConfiguration(
114-
halConfiguration.getIfAvailable(HalConfiguration::new));
115-
116-
if (resolvedConfiguration == null) {
117-
this.resolvedConfiguration = halFormsConfiguration.getIfAvailable(defaultConfig);
118-
}
119-
120-
return resolvedConfiguration;
129+
return configurationFactory.getConfiguration();
121130
}
122131
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.ArgumentMatchers.*;
20+
import static org.mockito.Mockito.*;
21+
22+
import java.util.stream.Stream;
23+
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.extension.ExtendWith;
26+
import org.mockito.Mock;
27+
import org.mockito.junit.jupiter.MockitoExtension;
28+
29+
/**
30+
* @author Oliver Drotbohm
31+
*/
32+
@ExtendWith(MockitoExtension.class)
33+
class MediaTypeConfigurationFactoryUnitTests {
34+
35+
@Mock MediaTypeConfigurationCustomizer<Object> first, second;
36+
37+
@Test // GH-2035
38+
void invokesCustomizers() {
39+
40+
var source = new Object();
41+
var afterFirst = new Object();
42+
var afterSecond = new Object();
43+
44+
doReturn(afterFirst).when(first).customize(source);
45+
doReturn(afterSecond).when(second).customize(afterFirst);
46+
47+
var factory = new MediaTypeConfigurationFactory<>(() -> source, () -> Stream.of(first, second));
48+
49+
assertThat(factory.getConfiguration()).isSameAs(afterSecond);
50+
51+
verify(first, times(1)).customize(source);
52+
verify(second, times(1)).customize(afterFirst);
53+
54+
assertThat(factory.getConfiguration()).isSameAs(afterSecond);
55+
56+
// Does not re-process source instance
57+
verify(first, times(1)).customize(any());
58+
verify(second, times(1)).customize(any());
59+
}
60+
}

0 commit comments

Comments
 (0)