diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java index e7df7318..d868296d 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelector.java @@ -20,26 +20,44 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.springframework.context.ResourceLoaderAware; import org.springframework.context.annotation.ImportSelector; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.type.AnnotationMetadata; import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; import org.springframework.http.MediaType; +import org.springframework.util.ClassUtils; /** * {@link ImportSelector} that looks up configuration classes from all {@link MediaTypeConfigurationProvider} * implementations listed in {@code META-INF/spring.factories}. * * @author Oliver Drotbohm + * @author Greg Turnquist */ -class HypermediaConfigurationImportSelector implements ImportSelector { +class HypermediaConfigurationImportSelector implements ImportSelector, ResourceLoaderAware { + + public static final String SPRING_TEST = "org.springframework.test.web.reactive.server.WebTestClient"; + + private ResourceLoader resourceLoader; /* * (non-Javadoc) - * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) + * @see org.springframework.context.ResourceLoaderAware#setResourceLoader(org.springframework.core.io.ResourceLoader) */ @Override + public void setResourceLoader(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + /* + * (non-Javadoc) + * @see org.springframework.context.annotation.ImportSelector#selectImports(org.springframework.core.type.AnnotationMetadata) + */ + @Override public String[] selectImports(AnnotationMetadata metadata) { Map attributes = metadata.getAnnotationAttributes(EnableHypermediaSupport.class.getName()); @@ -54,11 +72,17 @@ class HypermediaConfigurationImportSelector implements ImportSelector { MediaTypeConfigurationProvider.class, HypermediaConfigurationImportSelector.class.getClassLoader()); // Filter the ones supporting the given media types - return configurationProviders.stream() // + Stream imports = configurationProviders.stream() // .filter(it -> it.supportsAny(types)) // .map(MediaTypeConfigurationProvider::getConfiguration) // - .map(Class::getName) // - .toArray(String[]::new); + .map(Class::getName); + + // Conditionally apply other configurations + if (ClassUtils.isPresent(SPRING_TEST, resourceLoader.getClassLoader())) { + imports = Stream.concat(imports, Stream.of(WebTestHateoasConfiguration.class.getName())); + } + + return imports.toArray(String[]::new); } public String[] selectImports(List mediaType) { diff --git a/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java b/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java index 6c56b5c5..90bf557c 100644 --- a/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java +++ b/src/main/java/org/springframework/hateoas/config/HypermediaWebClientConfigurer.java @@ -37,7 +37,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; */ public class HypermediaWebClientConfigurer { - Consumer configurer; + final Consumer configurer; /** * Creates a new {@link HypermediaWebClientConfigurer} for the given {@link ObjectMapper} and diff --git a/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java index 59aa366e..b5017073 100644 --- a/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java +++ b/src/main/java/org/springframework/hateoas/config/WebClientHateoasConfiguration.java @@ -24,7 +24,6 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Lazy; import org.springframework.lang.NonNull; import org.springframework.web.reactive.function.client.WebClient; @@ -45,13 +44,6 @@ class WebClientHateoasConfiguration { return new HypermediaWebClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); } - @Bean - @Lazy - HypermediaWebTestClientConfigurer webTestClientConfigurer(ObjectProvider mapper, - List hypermediaTypes) { - return new HypermediaWebTestClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); - } - @Bean static HypermediaWebClientBeanPostProcessor webClientBeanPostProcessor(HypermediaWebClientConfigurer configurer) { return new HypermediaWebClientBeanPostProcessor(configurer); diff --git a/src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java b/src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java new file mode 100644 index 00000000..19d7234e --- /dev/null +++ b/src/main/java/org/springframework/hateoas/config/WebTestHateoasConfiguration.java @@ -0,0 +1,43 @@ +/* + * Copyright 2020 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.config; + +import java.util.List; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Configuration for Spring TEST-specific things + * + * @author Greg Turnquist + * @author Oliver Drotbohm + * @since 1.1 + */ +@Configuration(proxyBeanMethods = false) +class WebTestHateoasConfiguration { + + @Bean + @Lazy + HypermediaWebTestClientConfigurer webTestClientConfigurer(ObjectProvider mapper, + List hypermediaTypes) { + return new HypermediaWebTestClientConfigurer(mapper.getIfAvailable(ObjectMapper::new), hypermediaTypes); + } +} diff --git a/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java b/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java index c970208f..0a38d0a5 100644 --- a/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java +++ b/src/test/java/org/springframework/hateoas/config/HypermediaConfigurationImportSelectorUnitTest.java @@ -15,18 +15,24 @@ */ package org.springframework.hateoas.config; -import static org.assertj.core.api.AssertionsForInterfaceTypes.*; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +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 static org.springframework.hateoas.support.ContextTester.*; import java.util.Map; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.hateoas.client.LinkDiscoverer; import org.springframework.hateoas.mediatype.collectionjson.CollectionJsonLinkDiscoverer; import org.springframework.hateoas.mediatype.hal.HalLinkDiscoverer; import org.springframework.hateoas.mediatype.hal.forms.HalFormsLinkDiscoverer; import org.springframework.hateoas.mediatype.uber.UberLinkDiscoverer; +import org.springframework.hateoas.support.HidingClassLoader; +import org.springframework.instrument.classloading.ShadowingClassLoader; +import org.springframework.test.web.reactive.server.WebTestClientConfigurer; /** * Unit tests for {@link HypermediaConfigurationImportSelector}. @@ -89,6 +95,29 @@ class HypermediaConfigurationImportSelectorUnitTest { }); } + @Test // #1252 + void includesWebTestHateoasConfigurationIfWebTestClientIsOnTheClasspath() { + + withContext(HalConfig.class, context -> { + assertThat(context.getBean(WebTestHateoasConfiguration.class)).isNotNull(); + }); + } + + @Test // #1252 + void doesNotIncludeWebTestHateoasConfigurationIfWebTestClientIsNotOnTheClasspath() { + + HidingClassLoader delegate = HidingClassLoader.hide(WebTestClientConfigurer.class); + ShadowingClassLoader loader = new ShadowingClassLoader(delegate); + loader.excludePackage("org.springframework"); + + withContext(HalConfig.class, context -> { + + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> context.getBean(WebTestHateoasConfiguration.class)); + + }, loader); + } + @EnableHypermediaSupport(type = HAL) static class HalConfig { diff --git a/src/test/java/org/springframework/hateoas/support/ContextTester.java b/src/test/java/org/springframework/hateoas/support/ContextTester.java index acf1027a..04e8db83 100644 --- a/src/test/java/org/springframework/hateoas/support/ContextTester.java +++ b/src/test/java/org/springframework/hateoas/support/ContextTester.java @@ -15,21 +15,28 @@ */ package org.springframework.hateoas.support; +import java.util.function.Function; + +import org.springframework.lang.Nullable; import org.springframework.mock.web.MockServletContext; import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import java.util.function.Function; - /** * @author Greg Turnquist + * @author Oliver Drotbohm */ public class ContextTester { - public static void withContext(Class configuration, - ConsumerWithException consumer) throws E { + public static void withContext(Class configuration, // + ConsumerWithException consumer, // + @Nullable ClassLoader classLoader) throws E { try (AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext()) { + if (classLoader != null) { + context.setClassLoader(classLoader); + } + context.register(configuration); context.refresh(); @@ -37,6 +44,11 @@ public class ContextTester { } } + public static void withContext(Class configuration, + ConsumerWithException consumer) throws E { + withContext(configuration, consumer, null); + } + public static void withServletContext(Class configuration, ConsumerWithException consumer) throws E { diff --git a/src/test/java/org/springframework/hateoas/support/HidingClassLoader.java b/src/test/java/org/springframework/hateoas/support/HidingClassLoader.java new file mode 100644 index 00000000..42b24b7b --- /dev/null +++ b/src/test/java/org/springframework/hateoas/support/HidingClassLoader.java @@ -0,0 +1,101 @@ +/* + * Copyright 2017-2020 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.support; + +import java.net.URLClassLoader; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +import org.springframework.instrument.classloading.ShadowingClassLoader; +import org.springframework.util.Assert; + +/** + * is intended for testing code that depends on the presence/absence of certain classes. Classes can be: + *
    + *
  • shadowed: reloaded by this classloader no matter if they are loaded already by the SystemClassLoader
  • + *
  • hidden: not loaded by this classloader no matter if they are loaded already by the SystemClassLoader. Trying to + * load these classes results in a {@link ClassNotFoundException}
  • + *
  • all other classes get loaded by the SystemClassLoader
  • + *
+ * + * @author Jens Schauder + * @author Oliver Gierke + */ +public class HidingClassLoader extends ShadowingClassLoader { + + private final Collection hidden; + + HidingClassLoader(Collection hidden) { + + super(URLClassLoader.getSystemClassLoader(), false); + + this.hidden = hidden; + } + + /** + * Creates a new {@link HidingClassLoader} with the packages of the given classes hidden. + * + * @param packages must not be {@literal null}. + * @return + */ + public static HidingClassLoader hide(Class... packages) { + + Assert.notNull(packages, "Packages must not be null!"); + + return new HidingClassLoader(Arrays.stream(packages)// + .map(it -> it.getPackage().getName())// + .collect(Collectors.toList())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.classloadersupport.ShadowingClassLoader#loadClass(java.lang.String) + */ + @Override + public Class loadClass(String name) throws ClassNotFoundException { + + checkIfHidden(name); + return super.loadClass(name); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.classloadersupport.ShadowingClassLoader#isEligibleForShadowing(java.lang.String) + */ + @Override + protected boolean isEligibleForShadowing(String className) { + return isExcluded(className); + } + + /* + * (non-Javadoc) + * @see java.lang.ClassLoader#findClass(java.lang.String) + */ + @Override + protected Class findClass(String name) throws ClassNotFoundException { + + checkIfHidden(name); + return super.findClass(name); + } + + private void checkIfHidden(String name) throws ClassNotFoundException { + + if (hidden.stream().anyMatch(it -> name.startsWith(it))) { + throw new ClassNotFoundException(); + } + } +}