diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java index e07edfc602..6687722f9a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java @@ -16,12 +16,22 @@ package org.springframework.test.context.support; +import java.util.ArrayList; +import java.util.Arrays; import java.util.List; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.core.log.LogMessage; import org.springframework.core.style.ToStringCreator; import org.springframework.test.context.TestPropertySource; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.ResourceUtils; /** * {@code TestPropertySourceAttributes} encapsulates attributes declared @@ -37,48 +47,96 @@ import org.springframework.util.ObjectUtils; */ class TestPropertySourceAttributes { + private static final Log logger = LogFactory.getLog(TestPropertySourceAttributes.class); + + + private final int aggregateIndex; + private final Class declaringClass; - private final String[] locations; + private final List locations; private final boolean inheritLocations; - private final String[] properties; + private final List properties; private final boolean inheritProperties; - /** - * Create a new {@code TestPropertySourceAttributes} instance for the supplied - * values and enforce configuration rules. - * @param declaringClass the class that declared {@code @TestPropertySource} - * @param locations the merged {@link TestPropertySource#locations()} - * @param inheritLocations the {@link TestPropertySource#inheritLocations()} flag - * @param properties the merged {@link TestPropertySource#properties()} - * @param inheritProperties the {@link TestPropertySource#inheritProperties()} flag - * @since 5.2 - */ - TestPropertySourceAttributes(Class declaringClass, List locations, boolean inheritLocations, - List properties, boolean inheritProperties) { - - this(declaringClass, locations.toArray(new String[0]), inheritLocations, properties.toArray(new String[0]), - inheritProperties); + TestPropertySourceAttributes(MergedAnnotation annotation) { + this.aggregateIndex = annotation.getAggregateIndex(); + this.declaringClass = (Class) annotation.getSource(); + this.inheritLocations = annotation.getBoolean("inheritLocations"); + this.inheritProperties = annotation.getBoolean("inheritProperties"); + this.properties = new ArrayList<>(); + this.locations = new ArrayList<>(); + mergePropertiesAndLocations(annotation); } - private TestPropertySourceAttributes(Class declaringClass, String[] locations, boolean inheritLocations, - String[] properties, boolean inheritProperties) { - Assert.notNull(declaringClass, "'declaringClass' must not be null"); - Assert.isTrue(!ObjectUtils.isEmpty(locations) || !ObjectUtils.isEmpty(properties), - "Either 'locations' or 'properties' are required"); - - this.declaringClass = declaringClass; - this.locations = locations; - this.inheritLocations = inheritLocations; - this.properties = properties; - this.inheritProperties = inheritProperties; + boolean canMerge(MergedAnnotation annotation) { + return annotation.getAggregateIndex() == this.aggregateIndex; } + void merge(MergedAnnotation annotation) { + Assert.state((Class) annotation.getSource() == this.declaringClass, + () -> "Detected @TestPropertySource declarations within an aggregate index " + + "with different source: " + this.declaringClass + " and " + + annotation.getSource()); + logger.trace(LogMessage.format("Retrieved %s for declaring class [%s].", + annotation, this.declaringClass.getName())); + assertSameBooleanAttribute(this.inheritLocations, annotation, "inheritLocations"); + assertSameBooleanAttribute(this.inheritProperties, annotation, "inheritProperties"); + mergePropertiesAndLocations(annotation); + } + + private void assertSameBooleanAttribute(boolean expected, + MergedAnnotation annotation, String attribute) { + Assert.isTrue(expected == annotation.getBoolean(attribute), () -> String.format( + "Classes %s and %s must declare the same value for '%s' as other directly " + + "present or meta-present @TestPropertySource annotations", this.declaringClass.getName(), + ((Class) annotation.getSource()).getName(), attribute)); + } + + private void mergePropertiesAndLocations( + MergedAnnotation annotation) { + String[] locations = annotation.getStringArray("locations"); + String[] properties = annotation.getStringArray("properties"); + boolean prepend = annotation.getDistance() > 0; + if (ObjectUtils.isEmpty(locations) && ObjectUtils.isEmpty(properties)) { + addAll(prepend, this.locations, detectDefaultPropertiesFile(annotation)); + } + else { + addAll(prepend, this.locations, locations); + addAll(prepend, this.properties, properties); + } + } + + private void addAll(boolean prepend, List list, String... additions) { + list.addAll(prepend ? 0 : list.size(), Arrays.asList(additions)); + } + + private String detectDefaultPropertiesFile( + MergedAnnotation annotation) { + Class testClass = (Class) annotation.getSource(); + String resourcePath = ClassUtils.convertClassNameToResourcePath(testClass.getName()) + ".properties"; + ClassPathResource classPathResource = new ClassPathResource(resourcePath); + if (!classPathResource.exists()) { + String msg = String.format( + "Could not detect default properties file for test class [%s]: " + + "%s does not exist. Either declare the 'locations' or 'properties' attributes " + + "of @TestPropertySource or make the default properties file available.", + testClass.getName(), classPathResource); + logger.error(msg); + throw new IllegalStateException(msg); + } + String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath; + if (logger.isInfoEnabled()) { + logger.info(String.format("Detected default properties file \"%s\" for test class [%s]", + prefixedResourcePath, testClass.getName())); + } + return prefixedResourcePath; + } /** * Get the {@linkplain Class class} that declared {@code @TestPropertySource}. @@ -98,7 +156,7 @@ class TestPropertySourceAttributes { * @see TestPropertySource#locations */ String[] getLocations() { - return this.locations; + return this.locations.toArray(new String[0]); } /** @@ -119,7 +177,7 @@ class TestPropertySourceAttributes { * @see TestPropertySource#properties */ String[] getProperties() { - return this.properties; + return this.properties.toArray(new String[0]); } /** @@ -138,12 +196,12 @@ class TestPropertySourceAttributes { @Override public String toString() { return new ToStringCreator(this)// - .append("declaringClass", this.declaringClass.getName())// - .append("locations", ObjectUtils.nullSafeToString(this.locations))// - .append("inheritLocations", this.inheritLocations)// - .append("properties", ObjectUtils.nullSafeToString(this.properties))// - .append("inheritProperties", this.inheritProperties)// - .toString(); + .append("declaringClass", this.declaringClass.getName()) + .append("locations", this.locations) + .append("inheritLocations", this.inheritLocations) + .append("properties", this.properties) + .append("inheritProperties", this.inheritProperties) + .toString(); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 2b40af9361..df3658e036 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -20,14 +20,10 @@ import java.io.IOException; import java.io.StringReader; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; -import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; -import java.util.TreeMap; -import java.util.stream.Collectors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -41,16 +37,13 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.util.TestContextResourceUtils; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; -import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; /** @@ -75,19 +68,6 @@ public abstract class TestPropertySourceUtils { private static final Log logger = LogFactory.getLog(TestPropertySourceUtils.class); - /** - * Compares {@link MergedAnnotation} instances (presumably within the same - * aggregate index) by their meta-distance, in reverse order. - *

Using this {@link Comparator} to sort according to reverse meta-distance - * ensures that directly present annotations take precedence over meta-present - * annotations (within a given aggregate index). In other words, this follows - * the last-one-wins principle of overriding properties. - * @see MergedAnnotation#getAggregateIndex() - * @see MergedAnnotation#getDistance() - */ - private static final Comparator> reversedMetaDistanceComparator = - Comparator.> comparingInt(MergedAnnotation::getDistance).reversed(); - static MergedTestPropertySources buildMergedTestPropertySources(Class testClass) { MergedAnnotations mergedAnnotations = MergedAnnotations.from(testClass, SearchStrategy.EXHAUSTIVE); @@ -103,123 +83,21 @@ public abstract class TestPropertySourceUtils { private static List resolveTestPropertySourceAttributes( MergedAnnotations mergedAnnotations) { - // Group by aggregate index to ensure proper separation of inherited and local annotations. - Map>> aggregateIndexMap = mergedAnnotations - .stream(TestPropertySource.class) - .collect(Collectors.groupingBy(MergedAnnotation::getAggregateIndex, TreeMap::new, - Collectors.mapping(x -> x, Collectors.toList()))); - - // Stream the lists of annotations per aggregate index, merge each list into a - // single TestPropertySourceAttributes instance, and collect the results. - return aggregateIndexMap.values().stream() - .map(TestPropertySourceUtils::createTestPropertySourceAttributes) - .collect(Collectors.toList()); + List result = new ArrayList<>(); + mergedAnnotations.stream(TestPropertySource.class) + .forEach(annotation -> addOrMergeTestPropertySourceAttributes(result, annotation)); + return result; } - /** - * Create a merged {@link TestPropertySourceAttributes} instance from all - * annotations in the supplied list for a given aggregate index as if there - * were only one such annotation. - *

Within the supplied list, sort according to reversed meta-distance of - * the annotations from the declaring class. This ensures that directly present - * annotations take precedence over meta-present annotations within the current - * aggregate index. - *

If a given {@link TestPropertySource @TestPropertySource} does not - * declare properties or locations, an attempt will be made to detect a default - * properties file. - */ - private static TestPropertySourceAttributes createTestPropertySourceAttributes( - List> list) { + private static void addOrMergeTestPropertySourceAttributes( + List result, + MergedAnnotation annotation) { - list.sort(reversedMetaDistanceComparator); - - List locations = new ArrayList<>(); - List properties = new ArrayList<>(); - Class declaringClass = null; - Boolean inheritLocations = null; - Boolean inheritProperties = null; - - // Merge all @TestPropertySource annotations within the current - // aggregate index into a single TestPropertySourceAttributes instance, - // simultaneously ensuring that all such annotations have the same - // declaringClass, inheritLocations, and inheritProperties values. - for (MergedAnnotation mergedAnnotation : list) { - Class currentDeclaringClass = (Class) mergedAnnotation.getSource(); - if (declaringClass != null && !declaringClass.equals(currentDeclaringClass)) { - throw new IllegalStateException("Detected @TestPropertySource declarations within an aggregate index " + - "with different declaring classes: " + declaringClass.getName() + " and " + - currentDeclaringClass.getName()); - } - declaringClass = currentDeclaringClass; - - TestPropertySource testPropertySource = mergedAnnotation.synthesize(); - if (logger.isTraceEnabled()) { - logger.trace(String.format("Retrieved %s for declaring class [%s].", testPropertySource, - declaringClass.getName())); - } - - Boolean currentInheritLocations = testPropertySource.inheritLocations(); - assertConsistentValues(testPropertySource, declaringClass, "inheritLocations", inheritLocations, - currentInheritLocations); - inheritLocations = currentInheritLocations; - - Boolean currentInheritProperties = testPropertySource.inheritProperties(); - assertConsistentValues(testPropertySource, declaringClass, "inheritProperties", inheritProperties, - currentInheritProperties); - inheritProperties = currentInheritProperties; - - String[] currentLocations = testPropertySource.locations(); - String[] currentProperties = testPropertySource.properties(); - if (ObjectUtils.isEmpty(currentLocations) && ObjectUtils.isEmpty(currentProperties)) { - locations.add(detectDefaultPropertiesFile(declaringClass)); - } - else { - Collections.addAll(locations, currentLocations); - Collections.addAll(properties, currentProperties); - } - } - - TestPropertySourceAttributes attributes = new TestPropertySourceAttributes(declaringClass, locations, - inheritLocations, properties, inheritProperties); - if (logger.isTraceEnabled()) { - logger.trace(String.format("Resolved @TestPropertySource attributes %s for declaring class [%s].", - attributes, declaringClass.getName())); - } - return attributes; - } - - private static void assertConsistentValues(TestPropertySource testPropertySource, Class declaringClass, - String attributeName, Object trackedValue, Object currentValue) { - - Assert.isTrue((trackedValue == null || trackedValue.equals(currentValue)), - () -> String.format("%s on class [%s] must declare the same value for '%s' " + - "as other directly present or meta-present @TestPropertySource annotations on [%2$s].", - testPropertySource, declaringClass.getName(), attributeName)); - } - - /** - * Detect a default properties file for the supplied class, as specified - * in the class-level Javadoc for {@link TestPropertySource}. - */ - private static String detectDefaultPropertiesFile(Class testClass) { - String resourcePath = ClassUtils.convertClassNameToResourcePath(testClass.getName()) + ".properties"; - ClassPathResource classPathResource = new ClassPathResource(resourcePath); - - if (classPathResource.exists()) { - String prefixedResourcePath = ResourceUtils.CLASSPATH_URL_PREFIX + resourcePath; - if (logger.isInfoEnabled()) { - logger.info(String.format("Detected default properties file \"%s\" for test class [%s]", - prefixedResourcePath, testClass.getName())); - } - return prefixedResourcePath; + if (result.isEmpty() || !result.get(result.size()-1).canMerge(annotation)) { + result.add(new TestPropertySourceAttributes(annotation)); } else { - String msg = String.format("Could not detect default properties file for test class [%s]: " + - "%s does not exist. Either declare the 'locations' or 'properties' attributes " + - "of @TestPropertySource or make the default properties file available.", testClass.getName(), - classPathResource); - logger.error(msg); - throw new IllegalStateException(msg); + result.get(result.size() - 1).merge(annotation); } }