> L addAll(L list, L additions) {
- list.addAll(additions);
- return list;
+ /**
+ * {@link Collector#combiner() Combiner} for collections.
+ * This method is only invoked if the {@link java.util.stream.Stream} is
+ * processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
+ */
+ private static > C combiner(C collection, C additions) {
+ collection.addAll(additions);
+ return collection;
}
- private static MultiValueMap merge(MultiValueMap map,
- MultiValueMap additions) {
+ /**
+ * {@link Collector#combiner() Combiner} for multi-value maps.
+ * This method is only invoked if the {@link java.util.stream.Stream} is
+ * processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
+ */
+ private static MultiValueMap combiner(MultiValueMap map, MultiValueMap additions) {
map.addAll(additions);
return map;
}
diff --git a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java
index 48c0da67699..e805414c130 100644
--- a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java
+++ b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java
@@ -157,16 +157,20 @@ private Collection processDataBuffer(
int startIndex = buffer.readPosition();
int length = (endIndex - startIndex + 1);
DataBuffer slice = buffer.retainedSlice(startIndex, length);
- if (this.stripDelimiter) {
- slice.writePosition(slice.writePosition() - matcher.delimiter().length);
- }
result = (result != null ? result : new ArrayList<>());
if (chunks.isEmpty()) {
+ if (this.stripDelimiter) {
+ slice.writePosition(slice.writePosition() - matcher.delimiter().length);
+ }
result.add(slice);
}
else {
chunks.add(slice);
- result.add(buffer.factory().join(chunks));
+ DataBuffer joined = buffer.factory().join(chunks);
+ if (this.stripDelimiter) {
+ joined.writePosition(joined.writePosition() - matcher.delimiter().length);
+ }
+ result.add(joined);
chunks.clear();
}
buffer.readPosition(endIndex + 1);
diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java
index 2e966c7a28e..1134483e88a 100644
--- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java
+++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java
@@ -19,14 +19,15 @@
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Deque;
import java.util.HashSet;
-import java.util.LinkedHashMap;
-import java.util.LinkedHashSet;
-import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.CopyOnWriteArraySet;
import org.springframework.core.DecoratingProxy;
import org.springframework.core.ResolvableType;
@@ -499,9 +500,9 @@ public int compareTo(ConverterCacheKey other) {
*/
private static class Converters {
- private final Set globalConverters = new LinkedHashSet<>();
+ private final Set globalConverters = new CopyOnWriteArraySet<>();
- private final Map converters = new LinkedHashMap<>(36);
+ private final Map converters = new ConcurrentHashMap<>(256);
public void add(GenericConverter converter) {
Set convertibleTypes = converter.getConvertibleTypes();
@@ -512,8 +513,7 @@ public void add(GenericConverter converter) {
}
else {
for (ConvertiblePair convertiblePair : convertibleTypes) {
- ConvertersForPair convertersForPair = getMatchableConverters(convertiblePair);
- convertersForPair.add(converter);
+ getMatchableConverters(convertiblePair).add(converter);
}
}
}
@@ -651,7 +651,7 @@ private List getConverterStrings() {
*/
private static class ConvertersForPair {
- private final LinkedList converters = new LinkedList<>();
+ private final Deque converters = new ConcurrentLinkedDeque<>();
public void add(GenericConverter converter) {
this.converters.addFirst(converter);
diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java
index c618dfddbd6..6374f2768b9 100644
--- a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java
+++ b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -148,14 +148,21 @@ public boolean exists() {
*/
@Nullable
protected URL resolveURL() {
- if (this.clazz != null) {
- return this.clazz.getResource(this.path);
- }
- else if (this.classLoader != null) {
- return this.classLoader.getResource(this.path);
+ try {
+ if (this.clazz != null) {
+ return this.clazz.getResource(this.path);
+ }
+ else if (this.classLoader != null) {
+ return this.classLoader.getResource(this.path);
+ }
+ else {
+ return ClassLoader.getSystemResource(this.path);
+ }
}
- else {
- return ClassLoader.getSystemResource(this.path);
+ catch (IllegalArgumentException ex) {
+ // Should not happen according to the JDK's contract:
+ // see https://github.com/openjdk/jdk/pull/2662
+ return null;
}
}
diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java
index fb8c42aeeb0..d95e426d385 100644
--- a/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java
+++ b/spring-core/src/main/java/org/springframework/core/io/buffer/LimitedDataBufferList.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -54,11 +54,8 @@ public LimitedDataBufferList(int maxByteCount) {
@Override
public boolean add(DataBuffer buffer) {
- boolean result = super.add(buffer);
- if (result) {
- updateCount(buffer.readableByteCount());
- }
- return result;
+ updateCount(buffer.readableByteCount());
+ return super.add(buffer);
}
@Override
diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java
index 39d79fb2af5..69cd6613488 100644
--- a/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java
+++ b/spring-core/src/main/java/org/springframework/core/io/support/PathMatchingResourcePatternResolver.java
@@ -432,6 +432,9 @@ protected void addClassPathManifestEntries(Set result) {
// Possibly "c:" drive prefix on Windows, to be upper-cased for proper duplicate detection
filePath = StringUtils.capitalize(filePath);
}
+ // # can appear in directories/filenames, java.net.URL should not treat it as a fragment
+ filePath = StringUtils.replace(filePath, "#", "%23");
+ // Build URL that points to the root of the jar file
UrlResource jarResource = new UrlResource(ResourceUtils.JAR_URL_PREFIX +
ResourceUtils.FILE_URL_PREFIX + filePath + ResourceUtils.JAR_URL_SEPARATOR);
// Potentially overlapping with URLClassLoader.getURLs() result above!
diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java
index f8af7e8e65c..ce17e4db78b 100644
--- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java
+++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -32,6 +32,7 @@
* ASM method visitor that creates {@link SimpleMethodMetadata}.
*
* @author Phillip Webb
+ * @author Sam Brannen
* @since 5.2
*/
final class SimpleMethodMetadataReadingVisitor extends MethodVisitor {
@@ -144,14 +145,17 @@ public String toString() {
if (value == null) {
StringBuilder builder = new StringBuilder();
builder.append(this.declaringClassName);
- builder.append(".");
+ builder.append('.');
builder.append(this.name);
Type[] argumentTypes = Type.getArgumentTypes(this.descriptor);
- builder.append("(");
- for (Type type : argumentTypes) {
- builder.append(type.getClassName());
+ builder.append('(');
+ for (int i = 0; i < argumentTypes.length; i++) {
+ if (i != 0) {
+ builder.append(',');
+ }
+ builder.append(argumentTypes[i].getClassName());
}
- builder.append(")");
+ builder.append(')');
value = builder.toString();
this.toStringValue = value;
}
diff --git a/spring-core/src/main/java/org/springframework/objenesis/package-info.java b/spring-core/src/main/java/org/springframework/objenesis/package-info.java
index 8decb0e1225..9cddaf03494 100644
--- a/spring-core/src/main/java/org/springframework/objenesis/package-info.java
+++ b/spring-core/src/main/java/org/springframework/objenesis/package-info.java
@@ -1,6 +1,6 @@
/**
* Spring's repackaging of
- * Objenesis 3.0
+ * Objenesis 3.1
* (with SpringObjenesis entry point; for internal use only).
*
* This repackaging technique avoids any potential conflicts with
diff --git a/spring-core/src/main/java/org/springframework/util/ClassUtils.java b/spring-core/src/main/java/org/springframework/util/ClassUtils.java
index e570646f9e0..6018cbe107e 100644
--- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -88,13 +88,13 @@ public abstract class ClassUtils {
* Map with primitive wrapper type as key and corresponding primitive
* type as value, for example: Integer.class -> int.class.
*/
- private static final Map, Class>> primitiveWrapperTypeMap = new IdentityHashMap<>(8);
+ private static final Map, Class>> primitiveWrapperTypeMap = new IdentityHashMap<>(9);
/**
* Map with primitive type as key and corresponding wrapper
* type as value, for example: int.class -> Integer.class.
*/
- private static final Map, Class>> primitiveTypeToWrapperMap = new IdentityHashMap<>(8);
+ private static final Map, Class>> primitiveTypeToWrapperMap = new IdentityHashMap<>(9);
/**
* Map with primitive type name as key and corresponding primitive
@@ -1322,7 +1322,7 @@ public static Method getInterfaceMethodIfPossible(Method method) {
* Note that, despite being synthetic, bridge methods ({@link Method#isBridge()}) are considered
* as user-level methods since they are eventually pointing to a user-declared generic method.
* @param method the method to check
- * @return {@code true} if the method can be considered as user-declared; [@code false} otherwise
+ * @return {@code true} if the method can be considered as user-declared; {@code false} otherwise
*/
public static boolean isUserLevelMethod(Method method) {
Assert.notNull(method, "Method must not be null");
diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java
index ea9960441d8..f3af11b50a9 100644
--- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java
+++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -299,7 +299,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry,
@Override
@Nullable
- public V remove(Object key) {
+ public V remove(@Nullable Object key) {
return doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) {
@Override
@Nullable
@@ -316,7 +316,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry)
}
@Override
- public boolean remove(Object key, final Object value) {
+ public boolean remove(@Nullable Object key, final @Nullable Object value) {
Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) {
@Override
protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) {
@@ -333,7 +333,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e
}
@Override
- public boolean replace(K key, final V oldValue, final V newValue) {
+ public boolean replace(@Nullable K key, final @Nullable V oldValue, final @Nullable V newValue) {
Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) {
@Override
protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) {
@@ -349,7 +349,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e
@Override
@Nullable
- public V replace(K key, final V value) {
+ public V replace(@Nullable K key, final @Nullable V value) {
return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) {
@Override
@Nullable
@@ -474,7 +474,7 @@ protected final class Segment extends ReentrantLock {
* The total number of references contained in this segment. This includes chained
* references and references that have been garbage collected but not purged.
*/
- private final AtomicInteger count = new AtomicInteger(0);
+ private final AtomicInteger count = new AtomicInteger();
/**
* The threshold when resizing of the references should occur. When {@code count}
diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java
index 1aa4162470a..78425dd513e 100644
--- a/spring-core/src/main/java/org/springframework/util/MimeType.java
+++ b/spring-core/src/main/java/org/springframework/util/MimeType.java
@@ -16,6 +16,8 @@
package org.springframework.util;
+import java.io.IOException;
+import java.io.ObjectInputStream;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.BitSet;
@@ -104,7 +106,7 @@ public class MimeType implements Comparable, Serializable {
private final Map parameters;
@Nullable
- private Charset resolvedCharset;
+ private transient Charset resolvedCharset;
@Nullable
private volatile String toStringValue;
@@ -184,9 +186,9 @@ public MimeType(String type, String subtype, @Nullable Map param
this.subtype = subtype.toLowerCase(Locale.ENGLISH);
if (!CollectionUtils.isEmpty(parameters)) {
Map map = new LinkedCaseInsensitiveMap<>(parameters.size(), Locale.ENGLISH);
- parameters.forEach((attribute, value) -> {
- checkParameters(attribute, value);
- map.put(attribute, value);
+ parameters.forEach((parameter, value) -> {
+ checkParameters(parameter, value);
+ map.put(parameter, value);
});
this.parameters = Collections.unmodifiableMap(map);
}
@@ -210,11 +212,11 @@ private void checkToken(String token) {
}
}
- protected void checkParameters(String attribute, String value) {
- Assert.hasLength(attribute, "'attribute' must not be empty");
+ protected void checkParameters(String parameter, String value) {
+ Assert.hasLength(parameter, "'parameter' must not be empty");
Assert.hasLength(value, "'value' must not be empty");
- checkToken(attribute);
- if (PARAM_CHARSET.equals(attribute)) {
+ checkToken(parameter);
+ if (PARAM_CHARSET.equals(parameter)) {
if (this.resolvedCharset == null) {
this.resolvedCharset = Charset.forName(unquote(value));
}
@@ -569,6 +571,17 @@ public int compareTo(MimeType other) {
return 0;
}
+ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
+ // Rely on default serialization, just initialize state after deserialization.
+ ois.defaultReadObject();
+
+ // Initialize transient fields.
+ String charsetName = getParameter(PARAM_CHARSET);
+ if (charsetName != null) {
+ this.resolvedCharset = Charset.forName(unquote(charsetName));
+ }
+ }
+
/**
* Parse the given String value into a {@code MimeType} object,
diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
index b17d6f85fda..c35c0486025 100644
--- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
+++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -28,9 +28,11 @@
import org.springframework.lang.Nullable;
/**
- * Utility class for working with Strings that have placeholder values in them. A placeholder takes the form
- * {@code ${name}}. Using {@code PropertyPlaceholderHelper} these placeholders can be substituted for
- * user-supplied values. Values for substitution can be supplied using a {@link Properties} instance or
+ * Utility class for working with Strings that have placeholder values in them.
+ * A placeholder takes the form {@code ${name}}. Using {@code PropertyPlaceholderHelper}
+ * these placeholders can be substituted for user-supplied values.
+ *
+ *
Values for substitution can be supplied using a {@link Properties} instance or
* using a {@link PlaceholderResolver}.
*
* @author Juergen Hoeller
diff --git a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java
index 9b9decee735..89dfab86648 100644
--- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -384,7 +384,7 @@ else if (clazz.isInterface()) {
* @throws IllegalStateException if introspection fails
*/
public static Method[] getAllDeclaredMethods(Class> leafClass) {
- final List methods = new ArrayList<>(32);
+ final List methods = new ArrayList<>(20);
doWithMethods(leafClass, methods::add);
return methods.toArray(EMPTY_METHOD_ARRAY);
}
@@ -410,7 +410,7 @@ public static Method[] getUniqueDeclaredMethods(Class> leafClass) {
* @since 5.2
*/
public static Method[] getUniqueDeclaredMethods(Class> leafClass, @Nullable MethodFilter mf) {
- final List methods = new ArrayList<>(32);
+ final List methods = new ArrayList<>(20);
doWithMethods(leafClass, method -> {
boolean knownSignature = false;
Method methodBeingOverriddenWithCovariantReturnType = null;
@@ -505,12 +505,15 @@ private static List findConcreteMethodsOnInterfaces(Class> clazz) {
* @see java.lang.Object#equals(Object)
*/
public static boolean isEqualsMethod(@Nullable Method method) {
- if (method == null || !method.getName().equals("equals")) {
+ if (method == null) {
return false;
}
if (method.getParameterCount() != 1) {
return false;
}
+ if (!method.getName().equals("equals")) {
+ return false;
+ }
return method.getParameterTypes()[0] == Object.class;
}
@@ -519,7 +522,7 @@ public static boolean isEqualsMethod(@Nullable Method method) {
* @see java.lang.Object#hashCode()
*/
public static boolean isHashCodeMethod(@Nullable Method method) {
- return (method != null && method.getName().equals("hashCode") && method.getParameterCount() == 0);
+ return method != null && method.getParameterCount() == 0 && method.getName().equals("hashCode");
}
/**
@@ -527,7 +530,7 @@ public static boolean isHashCodeMethod(@Nullable Method method) {
* @see java.lang.Object#toString()
*/
public static boolean isToStringMethod(@Nullable Method method) {
- return (method != null && method.getName().equals("toString") && method.getParameterCount() == 0);
+ return (method != null && method.getParameterCount() == 0 && method.getName().equals("toString"));
}
/**
@@ -622,6 +625,7 @@ public static Field findField(Class> clazz, @Nullable String name, @Nullable C
* Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}.
* @param field the field to set
* @param target the target object on which to set the field
+ * (or {@code null} for a static field)
* @param value the value to set (may be {@code null})
*/
public static void setField(Field field, @Nullable Object target, @Nullable Object value) {
@@ -641,6 +645,7 @@ public static void setField(Field field, @Nullable Object target, @Nullable Obje
*
Thrown exceptions are handled via a call to {@link #handleReflectionException(Exception)}.
* @param field the field to get
* @param target the target object from which to get the field
+ * (or {@code null} for a static field)
* @return the field's current value
*/
@Nullable
diff --git a/spring-core/src/main/java/org/springframework/util/StringUtils.java b/spring-core/src/main/java/org/springframework/util/StringUtils.java
index 7867cda6713..7d562748953 100644
--- a/spring-core/src/main/java/org/springframework/util/StringUtils.java
+++ b/spring-core/src/main/java/org/springframework/util/StringUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -565,7 +565,7 @@ private static String changeFirstCharacterCase(String str, boolean capitalize) {
char[] chars = str.toCharArray();
chars[0] = updatedChar;
- return new String(chars, 0, chars.length);
+ return new String(chars);
}
/**
@@ -1341,8 +1341,8 @@ public static String arrayToDelimitedString(@Nullable Object[] arr, String delim
}
StringJoiner sj = new StringJoiner(delim);
- for (Object o : arr) {
- sj.add(String.valueOf(o));
+ for (Object elem : arr) {
+ sj.add(String.valueOf(elem));
}
return sj.toString();
}
diff --git a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java
index a41e1fdc629..ade19bdfd46 100644
--- a/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java
+++ b/spring-core/src/test/java/org/springframework/core/codec/StringDecoderTests.java
@@ -125,10 +125,10 @@ void decodeNewLine() {
);
testDecode(input, String.class, step -> step
- .expectNext("")
+ .expectNext("").as("1st")
.expectNext("abc")
.expectNext("defghi")
- .expectNext("")
+ .expectNext("").as("2nd")
.expectNext("jklmno")
.expectNext("pqr")
.expectNext("stuvwxyz")
@@ -136,6 +136,21 @@ void decodeNewLine() {
.verify());
}
+ @Test
+ void decodeNewlinesAcrossBuffers() {
+ Flux input = Flux.just(
+ stringBuffer("\r"),
+ stringBuffer("\n"),
+ stringBuffer("xyz")
+ );
+
+ testDecode(input, String.class, step -> step
+ .expectNext("")
+ .expectNext("xyz")
+ .expectComplete()
+ .verify());
+ }
+
@Test
void maxInMemoryLimit() {
Flux input = Flux.just(
diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java
index 7c71dc8b728..8615551b319 100644
--- a/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java
+++ b/spring-core/src/test/java/org/springframework/core/io/buffer/DataBufferUtilsTests.java
@@ -35,6 +35,8 @@
import java.util.concurrent.CountDownLatch;
import io.netty.buffer.ByteBuf;
+import io.netty.buffer.PooledByteBufAllocator;
+import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import org.reactivestreams.Subscription;
import reactor.core.publisher.BaseSubscriber;
@@ -834,6 +836,22 @@ void joinWithLimit(String displayName, DataBufferFactory bufferFactory) {
.verifyError(DataBufferLimitException.class);
}
+ @Test // gh-26060
+ void joinWithLimitDoesNotOverRelease() {
+ NettyDataBufferFactory bufferFactory = new NettyDataBufferFactory(PooledByteBufAllocator.DEFAULT);
+ byte[] bytes = "foo-bar-baz".getBytes(StandardCharsets.UTF_8);
+
+ NettyDataBuffer buffer = bufferFactory.allocateBuffer(bytes.length);
+ buffer.getNativeBuffer().retain(); // should be at 2 now
+ buffer.write(bytes);
+
+ Mono result = DataBufferUtils.join(Flux.just(buffer), 8);
+
+ StepVerifier.create(result).verifyError(DataBufferLimitException.class);
+ assertThat(buffer.getNativeBuffer().refCnt()).isEqualTo(1);
+ buffer.release();
+ }
+
@ParameterizedDataBufferAllocatingTest
void joinErrors(String displayName, DataBufferFactory bufferFactory) {
super.bufferFactory = bufferFactory;
diff --git a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java
index eeb816fe274..cb5c43ebe95 100644
--- a/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java
+++ b/spring-core/src/test/java/org/springframework/core/io/buffer/LimitedDataBufferListTests.java
@@ -17,9 +17,11 @@
import java.nio.charset.StandardCharsets;
-import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
/**
* Unit tests for {@link LimitedDataBufferList}.
* @author Rossen Stoyanchev
@@ -32,8 +34,10 @@ public class LimitedDataBufferListTests {
@Test
void limitEnforced() {
- Assertions.assertThatThrownBy(() -> new LimitedDataBufferList(5).add(toDataBuffer("123456")))
- .isInstanceOf(DataBufferLimitException.class);
+ LimitedDataBufferList list = new LimitedDataBufferList(5);
+
+ assertThatThrownBy(() -> list.add(toDataBuffer("123456"))).isInstanceOf(DataBufferLimitException.class);
+ assertThat(list).isEmpty();
}
@Test
diff --git a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java
index aa5fefc2e3c..f6a10f0e59c 100644
--- a/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java
+++ b/spring-core/src/test/java/org/springframework/util/MimeTypeTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -26,6 +26,7 @@
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
+import org.springframework.core.testfixture.io.SerializationTestUtils;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
@@ -267,13 +268,13 @@ void parseMimeTypeSingleQuotedParameterValue() {
assertThat(mimeType.getParameter("attr")).isEqualTo("'v>alue'");
}
- @Test // SPR-16630
+ @Test // SPR-16630
void parseMimeTypeWithSpacesAroundEquals() {
MimeType mimeType = MimeTypeUtils.parseMimeType("multipart/x-mixed-replace;boundary = --myboundary");
assertThat(mimeType.getParameter("boundary")).isEqualTo("--myboundary");
}
- @Test // SPR-16630
+ @Test // SPR-16630
void parseMimeTypeWithSpacesAroundEqualsAndQuotedValue() {
MimeType mimeType = MimeTypeUtils.parseMimeType("text/plain; foo = \" bar \" ");
assertThat(mimeType.getParameter("foo")).isEqualTo("\" bar \"");
@@ -303,14 +304,14 @@ void parseMimeTypes() {
assertThat(mimeTypes.size()).as("Invalid amount of mime types").isEqualTo(0);
}
- @Test // gh-23241
+ @Test // gh-23241
void parseMimeTypesWithTrailingComma() {
List mimeTypes = MimeTypeUtils.parseMimeTypes("text/plain, text/html,");
assertThat(mimeTypes).as("No mime types returned").isNotNull();
assertThat(mimeTypes.size()).as("Incorrect number of mime types").isEqualTo(2);
}
- @Test // SPR-17459
+ @Test // SPR-17459
void parseMimeTypesWithQuotedParameters() {
testWithQuotedParameters("foo/bar;param=\",\"");
testWithQuotedParameters("foo/bar;param=\"s,a,\"");
@@ -323,8 +324,9 @@ void parseMimeTypesWithQuotedParameters() {
private void testWithQuotedParameters(String... mimeTypes) {
String s = String.join(",", mimeTypes);
List actual = MimeTypeUtils.parseMimeTypes(s);
+
assertThat(actual.size()).isEqualTo(mimeTypes.length);
- for (int i=0; i < mimeTypes.length; i++) {
+ for (int i = 0; i < mimeTypes.length; i++) {
assertThat(actual.get(i).toString()).isEqualTo(mimeTypes[i]);
}
}
@@ -351,6 +353,7 @@ void compareTo() {
List result = new ArrayList<>(expected);
Random rnd = new Random();
+
// shuffle & sort 10 times
for (int i = 0; i < 10; i++) {
Collections.shuffle(result, rnd);
@@ -380,11 +383,7 @@ void compareToCaseSensitivity() {
assertThat(m2.compareTo(m1) != 0).as("Invalid comparison result").isTrue();
}
- /**
- * SPR-13157
- * @since 4.2
- */
- @Test
+ @Test // SPR-13157
void equalsIsCaseInsensitiveForCharsets() {
MimeType m1 = new MimeType("text", "plain", singletonMap("charset", "UTF-8"));
MimeType m2 = new MimeType("text", "plain", singletonMap("charset", "utf-8"));
@@ -394,4 +393,12 @@ void equalsIsCaseInsensitiveForCharsets() {
assertThat(m2.compareTo(m1)).isEqualTo(0);
}
+ @Test // gh-26127
+ void serialize() throws Exception {
+ MimeType original = new MimeType("text", "plain", StandardCharsets.UTF_8);
+ MimeType deserialized = (MimeType) SerializationTestUtils.serializeAndDeserialize(original);
+ assertThat(deserialized).isEqualTo(original);
+ assertThat(original).isEqualTo(deserialized);
+ }
+
}
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java
index 78bf5536ec3..ac7a9f0db16 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AstUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-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.
@@ -54,10 +54,9 @@ public static List getPropertyAccessorsToTry(
}
else {
if (targetType != null) {
- int pos = 0;
for (Class> clazz : targets) {
if (clazz == targetType) { // put exact matches on the front to be tried first?
- specificAccessors.add(pos++, resolver);
+ specificAccessors.add(resolver);
}
else if (clazz.isAssignableFrom(targetType)) { // put supertype matches at the end of the
// specificAccessor list
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java
index c70fcfb6da2..d42a375410f 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -57,7 +57,7 @@
*/
public class ConstructorReference extends SpelNodeImpl {
- private boolean isArrayConstructor = false;
+ private final boolean isArrayConstructor;
@Nullable
private SpelNodeImpl[] dimensions;
@@ -208,14 +208,14 @@ public String toStringAST() {
StringBuilder sb = new StringBuilder("new ");
int index = 0;
sb.append(getChild(index++).toStringAST());
- sb.append("(");
+ sb.append('(');
for (int i = index; i < getChildCount(); i++) {
if (i > index) {
- sb.append(",");
+ sb.append(',');
}
sb.append(getChild(i).toStringAST());
}
- sb.append(")");
+ sb.append(')');
return sb.toString();
}
@@ -234,6 +234,7 @@ private TypedValue createArray(ExpressionState state) throws EvaluationException
FormatHelper.formatClassNameForMessage(
intendedArrayType != null ? intendedArrayType.getClass() : null));
}
+
String type = (String) intendedArrayType;
Class> componentType;
TypeCode arrayTypeCode = TypeCode.forName(type);
@@ -243,7 +244,8 @@ private TypedValue createArray(ExpressionState state) throws EvaluationException
else {
componentType = arrayTypeCode.getType();
}
- Object newArray;
+
+ Object newArray = null;
if (!hasInitializer()) {
// Confirm all dimensions were specified (for example [3][][5] is missing the 2nd dimension)
if (this.dimensions != null) {
@@ -252,23 +254,22 @@ private TypedValue createArray(ExpressionState state) throws EvaluationException
throw new SpelEvaluationException(getStartPosition(), SpelMessage.MISSING_ARRAY_DIMENSION);
}
}
- }
- TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter();
-
- // Shortcut for 1 dimensional
- if (this.dimensions.length == 1) {
- TypedValue o = this.dimensions[0].getTypedValue(state);
- int arraySize = ExpressionUtils.toInt(typeConverter, o);
- newArray = Array.newInstance(componentType, arraySize);
- }
- else {
- // Multi-dimensional - hold onto your hat!
- int[] dims = new int[this.dimensions.length];
- for (int d = 0; d < this.dimensions.length; d++) {
- TypedValue o = this.dimensions[d].getTypedValue(state);
- dims[d] = ExpressionUtils.toInt(typeConverter, o);
+ TypeConverter typeConverter = state.getEvaluationContext().getTypeConverter();
+ if (this.dimensions.length == 1) {
+ // Shortcut for 1-dimensional
+ TypedValue o = this.dimensions[0].getTypedValue(state);
+ int arraySize = ExpressionUtils.toInt(typeConverter, o);
+ newArray = Array.newInstance(componentType, arraySize);
+ }
+ else {
+ // Multi-dimensional - hold onto your hat!
+ int[] dims = new int[this.dimensions.length];
+ for (int d = 0; d < this.dimensions.length; d++) {
+ TypedValue o = this.dimensions[d].getTypedValue(state);
+ dims[d] = ExpressionUtils.toInt(typeConverter, o);
+ }
+ newArray = Array.newInstance(componentType, dims);
}
- newArray = Array.newInstance(componentType, dims);
}
}
else {
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java
index a83bf5dbe02..847da65a735 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-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.
@@ -63,18 +63,20 @@
* Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.
*
* @author Andy Clement
+ * @author Juergen Hoeller
* @since 4.1
*/
public final class SpelCompiler implements Opcodes {
- private static final Log logger = LogFactory.getLog(SpelCompiler.class);
-
private static final int CLASSES_DEFINED_LIMIT = 100;
+ private static final Log logger = LogFactory.getLog(SpelCompiler.class);
+
// A compiler is created for each classloader, it manages a child class loader of that
// classloader and the child is used to load the compiled expressions.
private static final Map compilers = new ConcurrentReferenceHashMap<>();
+
// The child ClassLoader used to load the compiled expression classes
private ChildClassLoader ccl;
@@ -90,7 +92,7 @@ private SpelCompiler(@Nullable ClassLoader classloader) {
/**
* Attempt compilation of the supplied expression. A check is made to see
* if it is compilable before compilation proceeds. The check involves
- * visiting all the nodes in the expression Ast and ensuring enough state
+ * visiting all the nodes in the expression AST and ensuring enough state
* is known about them that bytecode can be generated for them.
* @param expression the expression to compile
* @return an instance of the class implementing the compiled expression,
@@ -125,7 +127,7 @@ private int getNextSuffix() {
/**
* Generate the class that encapsulates the compiled expression and define it.
- * The generated class will be a subtype of CompiledExpression.
+ * The generated class will be a subtype of CompiledExpression.
* @param expressionToCompile the expression to be compiled
* @return the expression call, or {@code null} if the decision was to opt out of
* compilation during code generation
@@ -150,7 +152,7 @@ private Class extends CompiledExpression> createExpressionClass(SpelNodeImpl e
// Create getValue() method
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
- new String[ ]{"org/springframework/expression/EvaluationException"});
+ new String[] {"org/springframework/expression/EvaluationException"});
mv.visitCode();
CodeFlow cf = new CodeFlow(className, cw);
@@ -187,7 +189,7 @@ private Class extends CompiledExpression> createExpressionClass(SpelNodeImpl e
/**
* Load a compiled expression class. Makes sure the classloaders aren't used too much
- * because they anchor compiled classes in memory and prevent GC. If you have expressions
+ * because they anchor compiled classes in memory and prevent GC. If you have expressions
* continually recompiling over time then by replacing the classloader periodically
* at least some of the older variants can be garbage collected.
* @param name the name of the class
@@ -202,6 +204,7 @@ private Class extends CompiledExpression> loadClass(String name, byte[] bytes)
return (Class extends CompiledExpression>) this.ccl.defineClass(name, bytes);
}
+
/**
* Factory method for compiler instances. The returned SpelCompiler will
* attach a class loader as the child of the given class loader and this
@@ -222,10 +225,12 @@ public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) {
}
/**
- * Request that an attempt is made to compile the specified expression. It may fail if
- * components of the expression are not suitable for compilation or the data types
- * involved are not suitable for compilation. Used for testing.
- * @return true if the expression was successfully compiled
+ * Request that an attempt is made to compile the specified expression.
+ * It may fail if components of the expression are not suitable for compilation
+ * or the data types involved are not suitable for compilation. Used for testing.
+ * @param expression the expression to compile
+ * @return {@code true} if the expression was successfully compiled,
+ * {@code false} otherwise
*/
public static boolean compile(Expression expression) {
return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());
@@ -256,18 +261,21 @@ public ChildClassLoader(@Nullable ClassLoader classLoader) {
super(NO_URLS, classLoader);
}
- int getClassesDefinedCount() {
- return this.classesDefinedCount;
- }
-
public Class> defineClass(String name, byte[] bytes) {
Class> clazz = super.defineClass(name, bytes, 0, bytes.length);
this.classesDefinedCount++;
return clazz;
}
+
+ public int getClassesDefinedCount() {
+ return this.classesDefinedCount;
+ }
}
+ /**
+ * An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader.
+ */
private class ExpressionClassWriter extends ClassWriter {
public ExpressionClassWriter() {
diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java
index e8cf3dad604..468e25540b4 100644
--- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java
+++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java
@@ -47,7 +47,7 @@
/**
* A powerful {@link PropertyAccessor} that uses reflection to access properties
- * for reading and possibly also for writing.
+ * for reading and possibly also for writing on a target instance.
*
* A property can be referenced through a public getter method (when being read)
* or a public setter method (when being written), and also as a public field.
@@ -98,8 +98,8 @@ public ReflectivePropertyAccessor() {
}
/**
- * Create a new property accessor for reading and possibly writing.
- * @param allowWrite whether to also allow for write operations
+ * Create a new property accessor for reading and possibly also writing.
+ * @param allowWrite whether to allow write operations on a target instance
* @since 4.3.15
* @see #canWrite
*/
@@ -623,8 +623,8 @@ public int hashCode() {
@Override
public String toString() {
- return "CacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property + ", " +
- this.property + ", targetIsClass=" + this.targetIsClass + "]";
+ return "PropertyCacheKey [clazz=" + this.clazz.getName() + ", property=" + this.property +
+ ", targetIsClass=" + this.targetIsClass + "]";
}
@Override
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java
index 23b397b30fa..b114826270b 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java
@@ -76,7 +76,6 @@
*/
public class SpelReproTests extends AbstractExpressionTests {
-
@Test
public void NPE_SPR5661() {
evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class);
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java
index 418ff975ed6..17939f7a0f2 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/Person.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -16,9 +16,10 @@
package org.springframework.expression.spel.testresources;
-///CLOVER:OFF
public class Person {
+
private String privateName;
+
Company company;
public Person(String name) {
@@ -41,4 +42,5 @@ public void setName(String n) {
public Company getCompany() {
return company;
}
+
}
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java
index 3bc88ff9f43..7beb41441aa 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestAddress.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -19,19 +19,25 @@
import java.util.List;
public class TestAddress{
- private String street;
- private List crossStreets;
-
- public String getStreet() {
- return street;
- }
- public void setStreet(String street) {
- this.street = street;
- }
- public List getCrossStreets() {
- return crossStreets;
- }
- public void setCrossStreets(List crossStreets) {
- this.crossStreets = crossStreets;
- }
+
+ private String street;
+
+ private List crossStreets;
+
+ public String getStreet() {
+ return street;
+ }
+
+ public void setStreet(String street) {
+ this.street = street;
}
+
+ public List getCrossStreets() {
+ return crossStreets;
+ }
+
+ public void setCrossStreets(List crossStreets) {
+ this.crossStreets = crossStreets;
+ }
+
+}
diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java
index ad1f58480e5..e9470f26c8b 100644
--- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java
+++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/TestPerson.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -17,19 +17,25 @@
package org.springframework.expression.spel.testresources;
public class TestPerson {
- private String name;
- private TestAddress address;
-
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- public TestAddress getAddress() {
- return address;
- }
- public void setAddress(TestAddress address) {
- this.address = address;
- }
+
+ private String name;
+
+ private TestAddress address;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
}
+
+ public TestAddress getAddress() {
+ return address;
+ }
+
+ public void setAddress(TestAddress address) {
+ this.address = address;
+ }
+
+}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java
index b5860e82fef..71b4da1d21c 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/BeanPropertyRowMapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -223,10 +223,11 @@ protected void initialize(Class mappedClass) {
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) {
if (pd.getWriteMethod() != null) {
- this.mappedFields.put(lowerCaseName(pd.getName()), pd);
- String underscoredName = underscoreName(pd.getName());
- if (!lowerCaseName(pd.getName()).equals(underscoredName)) {
- this.mappedFields.put(underscoredName, pd);
+ String lowerCaseName = lowerCaseName(pd.getName());
+ this.mappedFields.put(lowerCaseName, pd);
+ String underscoreName = underscoreName(pd.getName());
+ if (!lowerCaseName.equals(underscoreName)) {
+ this.mappedFields.put(underscoreName, pd);
}
this.mappedProperties.add(pd.getName());
}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java
index c7aab55d29e..805698dcaa5 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -30,7 +30,6 @@
import org.springframework.dao.InvalidDataAccessApiUsageException;
import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
/**
* Helper class that efficiently creates multiple {@link PreparedStatementCreator}
@@ -200,9 +199,8 @@ public PreparedStatementCreatorImpl(List> parameters) {
public PreparedStatementCreatorImpl(String actualSql, List> parameters) {
this.actualSql = actualSql;
- Assert.notNull(parameters, "Parameters List must not be null");
this.parameters = parameters;
- if (this.parameters.size() != declaredParameters.size()) {
+ if (parameters.size() != declaredParameters.size()) {
// Account for named parameters being used multiple times
Set names = new HashSet<>();
for (int i = 0; i < parameters.size(); i++) {
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java
index c8295860ac5..27776fad7f4 100755
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java
@@ -637,21 +637,23 @@ public String createCallString() {
schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
}
- String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
if (isFunction() || isReturnValueRequired()) {
- callString = new StringBuilder().append("{? = call ").
- append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
- append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
- append(procedureNameToUse).append("(");
+ callString = new StringBuilder("{? = call ");
parameterCount = -1;
}
else {
- callString = new StringBuilder().append("{call ").
- append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
- append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
- append(procedureNameToUse).append("(");
+ callString = new StringBuilder("{call ");
}
+ if (StringUtils.hasLength(catalogNameToUse)) {
+ callString.append(catalogNameToUse).append(".");
+ }
+ if (StringUtils.hasLength(schemaNameToUse)) {
+ callString.append(schemaNameToUse).append(".");
+ }
+ callString.append(this.metaDataProvider.procedureNameToUse(getProcedureName()));
+ callString.append("(");
+
for (SqlParameter parameter : this.callParameters) {
if (!parameter.isResultsParameter()) {
if (parameterCount > 0) {
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java
index f0b6991f682..8a9b2d0a3fb 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -39,6 +39,7 @@
*
* @author Thomas Risberg
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 2.5
*/
public class GenericCallMetaDataProvider implements CallMetaDataProvider {
@@ -414,8 +415,15 @@ else if ("Oracle".equals(databaseMetaData.getDatabaseProductName())) {
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
- logger.warn("Error while retrieving meta-data for procedure columns: " + ex);
+ logger.warn("Error while retrieving meta-data for procedure columns. " +
+ "Consider declaring explicit parameters -- for example, via SimpleJdbcCall#addDeclaredParameter().",
+ ex);
}
+ // Although we could invoke `this.callParameterMetaData.clear()` so that
+ // we don't retain a partial list of column names (like we do in
+ // GenericTableMetaDataProvider.processTableColumns(...)), we choose
+ // not to do that here, since invocation of the stored procedure will
+ // likely fail anyway with an incorrect argument list.
}
}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java
index b69f9bed0a3..5228cb9b05c 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -39,6 +39,7 @@
*
* @author Thomas Risberg
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 2.5
*/
public class GenericTableMetaDataProvider implements TableMetaDataProvider {
@@ -422,8 +423,12 @@ private void processTableColumns(DatabaseMetaData databaseMetaData, TableMetaDat
}
catch (SQLException ex) {
if (logger.isWarnEnabled()) {
- logger.warn("Error while retrieving meta-data for table columns: " + ex.getMessage());
+ logger.warn("Error while retrieving meta-data for table columns. " +
+ "Consider specifying explicit column names -- for example, via SimpleJdbcInsert#usingColumns().",
+ ex);
}
+ // Clear the metadata so that we don't retain a partial list of column names
+ this.tableParameterMetaData.clear();
}
finally {
JdbcUtils.closeResultSet(tableColumns);
@@ -432,7 +437,7 @@ private void processTableColumns(DatabaseMetaData databaseMetaData, TableMetaDat
/**
- * Inner class representing table meta-data.
+ * Class representing table meta-data.
*/
private static class TableMetaData {
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java
index ca57f528749..34853e9cfd8 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -43,6 +43,7 @@
*
* @author Thomas Risberg
* @author Juergen Hoeller
+ * @author Sam Brannen
* @since 2.5
*/
public class TableMetaDataContext {
@@ -302,8 +303,12 @@ public String createInsertString(String... generatedKeyNames) {
}
}
else {
- throw new InvalidDataAccessApiUsageException("Unable to locate columns for table '" +
- getTableName() + "' so an insert statement can't be generated");
+ String message = "Unable to locate columns for table '" + getTableName()
+ + "' so an insert statement can't be generated.";
+ if (isAccessTableColumnMetaData()) {
+ message += " Consider specifying explicit column names -- for example, via SimpleJdbcInsert#usingColumns().";
+ }
+ throw new InvalidDataAccessApiUsageException(message);
}
}
String params = String.join(", ", Collections.nCopies(columnCount, "?"));
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java
index 854d980cacd..a1cabecba60 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProviderFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -51,7 +51,6 @@ public static TableMetaDataProvider createMetaDataProvider(DataSource dataSource
try {
return JdbcUtils.extractDatabaseMetaData(dataSource, databaseMetaData -> {
String databaseProductName = JdbcUtils.commonDatabaseName(databaseMetaData.getDatabaseProductName());
- boolean accessTableColumnMetaData = context.isAccessTableColumnMetaData();
TableMetaDataProvider provider;
if ("Oracle".equals(databaseProductName)) {
@@ -70,15 +69,17 @@ else if ("HSQL Database Engine".equals(databaseProductName)) {
else {
provider = new GenericTableMetaDataProvider(databaseMetaData);
}
-
if (logger.isDebugEnabled()) {
logger.debug("Using " + provider.getClass().getSimpleName());
}
+
provider.initializeWithMetaData(databaseMetaData);
- if (accessTableColumnMetaData) {
+
+ if (context.isAccessTableColumnMetaData()) {
provider.initializeWithTableColumnMetaData(databaseMetaData,
context.getCatalogName(), context.getSchemaName(), context.getTableName());
}
+
return provider;
});
}
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java
index 068d4d01ac0..1325cdedf5e 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -36,6 +36,7 @@
*
* @author Thomas Risberg
* @author Juergen Hoeller
+ * @author Yanming Zhou
* @since 2.0
*/
public abstract class NamedParameterUtils {
@@ -83,7 +84,7 @@ public static ParsedSql parseSqlStatement(final String sql) {
Assert.notNull(sql, "SQL must not be null");
Set namedParameters = new HashSet<>();
- String sqlToUse = sql;
+ StringBuilder sqlToUse = new StringBuilder(sql);
List parameterList = new ArrayList<>();
char[] statement = sql.toCharArray();
@@ -155,7 +156,7 @@ public static ParsedSql parseSqlStatement(final String sql) {
int j = i + 1;
if (j < statement.length && statement[j] == ':') {
// escaped ":" should be skipped
- sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
+ sqlToUse.deleteCharAt(i - escapes);
escapes++;
i = i + 2;
continue;
@@ -174,7 +175,7 @@ public static ParsedSql parseSqlStatement(final String sql) {
}
i++;
}
- ParsedSql parsedSql = new ParsedSql(sqlToUse);
+ ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
for (ParameterHolder ph : parameterList) {
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
}
@@ -345,9 +346,15 @@ public static Object[] buildValueArray(
for (int i = 0; i < paramNames.size(); i++) {
String paramName = paramNames.get(i);
try {
- Object value = paramSource.getValue(paramName);
SqlParameter param = findParameter(declaredParams, paramName, i);
- paramArray[i] = (param != null ? new SqlParameterValue(param, value) : value);
+ Object paramValue = paramSource.getValue(paramName);
+ if (paramValue instanceof SqlParameterValue) {
+ paramArray[i] = paramValue;
+ }
+ else {
+ paramArray[i] = (param != null ? new SqlParameterValue(param, paramValue) :
+ SqlParameterSourceUtils.getTypedValue(paramSource, paramName));
+ }
}
catch (IllegalArgumentException ex) {
throw new InvalidDataAccessApiUsageException(
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java
index bb9a000df3e..e2bd60e05ff 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-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.
@@ -44,7 +44,6 @@ public abstract class SqlParameterSourceUtils {
* @see BeanPropertySqlParameterSource
* @see NamedParameterJdbcTemplate#batchUpdate(String, SqlParameterSource[])
*/
- @SuppressWarnings("unchecked")
public static SqlParameterSource[] createBatch(Object... candidates) {
return createBatch(Arrays.asList(candidates));
}
@@ -93,17 +92,13 @@ public static SqlParameterSource[] createBatch(Map[] valueMaps) {
* @param source the source of parameter values and type information
* @param parameterName the name of the parameter
* @return the value object
+ * @see SqlParameterValue
*/
@Nullable
public static Object getTypedValue(SqlParameterSource source, String parameterName) {
int sqlType = source.getSqlType(parameterName);
if (sqlType != SqlParameterSource.TYPE_UNKNOWN) {
- if (source.getTypeName(parameterName) != null) {
- return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName));
- }
- else {
- return new SqlParameterValue(sqlType, source.getValue(parameterName));
- }
+ return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName));
}
else {
return source.getValue(parameterName);
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java
index 6e0d8aafa40..edebf338c11 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-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.
@@ -70,7 +70,7 @@ public abstract class AbstractJdbcCall {
* Has this operation been compiled? Compilation means at least checking
* that a DataSource or JdbcTemplate has been provided.
*/
- private volatile boolean compiled = false;
+ private volatile boolean compiled;
/** The generated string used for call statement. */
@Nullable
@@ -433,7 +433,7 @@ protected List getCallParameters() {
/**
* Match the provided in parameter values with registered parameters and
* parameters defined via meta-data processing.
- * @param parameterSource the parameter vakues provided as a {@link SqlParameterSource}
+ * @param parameterSource the parameter values provided as a {@link SqlParameterSource}
* @return a Map with parameter names and values
*/
protected Map matchInParameterValuesWithCallParameters(SqlParameterSource parameterSource) {
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java
index ce40e961697..8887110eff7 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java
@@ -70,7 +70,7 @@ public abstract class AbstractJdbcInsert {
/** Context used to retrieve and manage database meta-data. */
private final TableMetaDataContext tableMetaDataContext = new TableMetaDataContext();
- /** List of columns objects to be used in insert statement. */
+ /** List of column names to be used in insert statement. */
private final List declaredColumns = new ArrayList<>();
/** The names of the columns holding the generated key. */
diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java
index fa87e08f9d5..f20b5d857b9 100644
--- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java
+++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -102,209 +102,208 @@ public abstract class ScriptUtils {
/**
- * Split an SQL script into separate statements delimited by the provided
- * separator character. Each individual statement will be added to the
- * provided {@code List}.
- * Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the
- * comment prefix; any text beginning with the comment prefix and extending to
- * the end of the line will be omitted from the output. Similarly,
- * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and
- * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the
- * start and end block comment delimiters: any text enclosed
- * in a block comment will be omitted from the output. In addition, multiple
- * adjacent whitespace characters will be collapsed into a single space.
- * @param script the SQL script
- * @param separator character separating each statement (typically a ';')
- * @param statements the list that will contain the individual statements
- * @throws ScriptException if an error occurred while splitting the SQL script
- * @see #splitSqlScript(String, String, List)
- * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List)
+ * Execute the given SQL script using default settings for statement
+ * separators, comment delimiters, and exception handling flags.
+ *
Statement separators and comments will be removed before executing
+ * individual statements within the supplied script.
+ *
Warning: this method does not release the
+ * provided {@link Connection}.
+ * @param connection the JDBC connection to use to execute the script; already
+ * configured and ready to use
+ * @param resource the resource to load the SQL script from; encoded with the
+ * current platform's default encoding
+ * @throws ScriptException if an error occurred while executing the SQL script
+ * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #DEFAULT_COMMENT_PREFIX
+ * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
+ * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
- public static void splitSqlScript(String script, char separator, List statements) throws ScriptException {
- splitSqlScript(script, String.valueOf(separator), statements);
+ public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException {
+ executeSqlScript(connection, new EncodedResource(resource));
}
/**
- * Split an SQL script into separate statements delimited by the provided
- * separator string. Each individual statement will be added to the
- * provided {@code List}.
- * Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the
- * comment prefix; any text beginning with the comment prefix and extending to
- * the end of the line will be omitted from the output. Similarly,
- * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and
- * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the
- * start and end block comment delimiters: any text enclosed
- * in a block comment will be omitted from the output. In addition, multiple
- * adjacent whitespace characters will be collapsed into a single space.
- * @param script the SQL script
- * @param separator text separating each statement
- * (typically a ';' or newline character)
- * @param statements the list that will contain the individual statements
- * @throws ScriptException if an error occurred while splitting the SQL script
- * @see #splitSqlScript(String, char, List)
- * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List)
+ * Execute the given SQL script using default settings for statement
+ * separators, comment delimiters, and exception handling flags.
+ *
Statement separators and comments will be removed before executing
+ * individual statements within the supplied script.
+ *
Warning: this method does not release the
+ * provided {@link Connection}.
+ * @param connection the JDBC connection to use to execute the script; already
+ * configured and ready to use
+ * @param resource the resource (potentially associated with a specific encoding)
+ * to load the SQL script from
+ * @throws ScriptException if an error occurred while executing the SQL script
+ * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #DEFAULT_COMMENT_PREFIX
+ * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
+ * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
- public static void splitSqlScript(String script, String separator, List statements) throws ScriptException {
- splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
- DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements);
+ public static void executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
+ executeSqlScript(connection, resource, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR,
+ DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
}
/**
- * Split an SQL script into separate statements delimited by the provided
- * separator string. Each individual statement will be added to the provided
- * {@code List}.
- * Within the script, the provided {@code commentPrefix} will be honored:
- * any text beginning with the comment prefix and extending to the end of the
- * line will be omitted from the output. Similarly, the provided
- * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
- * delimiters will be honored: any text enclosed in a block comment will be
- * omitted from the output. In addition, multiple adjacent whitespace characters
- * will be collapsed into a single space.
- * @param resource the resource from which the script was read
- * @param script the SQL script
- * @param separator text separating each statement
- * (typically a ';' or newline character)
- * @param commentPrefix the prefix that identifies SQL line comments
- * (typically "--")
- * @param blockCommentStartDelimiter the start block comment delimiter;
- * never {@code null} or empty
- * @param blockCommentEndDelimiter the end block comment delimiter;
- * never {@code null} or empty
- * @param statements the list that will contain the individual statements
- * @throws ScriptException if an error occurred while splitting the SQL script
+ * Execute the given SQL script.
+ *
Statement separators and comments will be removed before executing
+ * individual statements within the supplied script.
+ *
Warning: this method does not release the
+ * provided {@link Connection}.
+ * @param connection the JDBC connection to use to execute the script; already
+ * configured and ready to use
+ * @param resource the resource (potentially associated with a specific encoding)
+ * to load the SQL script from
+ * @param continueOnError whether or not to continue without throwing an exception
+ * in the event of an error
+ * @param ignoreFailedDrops whether or not to continue in the event of specifically
+ * an error on a {@code DROP} statement
+ * @param commentPrefix the prefix that identifies single-line comments in the
+ * SQL script (typically "--")
+ * @param separator the script statement separator; defaults to
+ * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to
+ * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
+ * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a
+ * single statement without a separator
+ * @param blockCommentStartDelimiter the start block comment delimiter
+ * @param blockCommentEndDelimiter the end block comment delimiter
+ * @throws ScriptException if an error occurred while executing the SQL script
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #FALLBACK_STATEMENT_SEPARATOR
+ * @see #EOF_STATEMENT_SEPARATOR
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
- public static void splitSqlScript(@Nullable EncodedResource resource, String script,
- String separator, String commentPrefix, String blockCommentStartDelimiter,
- String blockCommentEndDelimiter, List statements) throws ScriptException {
+ public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
+ boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator,
+ String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
- Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
- splitSqlScript(resource, script, separator, new String[] { commentPrefix },
- blockCommentStartDelimiter, blockCommentEndDelimiter, statements);
+ executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops,
+ new String[] { commentPrefix }, separator, blockCommentStartDelimiter,
+ blockCommentEndDelimiter);
}
/**
- * Split an SQL script into separate statements delimited by the provided
- * separator string. Each individual statement will be added to the provided
- * {@code List}.
- * Within the script, the provided {@code commentPrefixes} will be honored:
- * any text beginning with one of the comment prefixes and extending to the
- * end of the line will be omitted from the output. Similarly, the provided
- * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
- * delimiters will be honored: any text enclosed in a block comment will be
- * omitted from the output. In addition, multiple adjacent whitespace characters
- * will be collapsed into a single space.
- * @param resource the resource from which the script was read
- * @param script the SQL script
- * @param separator text separating each statement
- * (typically a ';' or newline character)
- * @param commentPrefixes the prefixes that identify SQL line comments
- * (typically "--")
- * @param blockCommentStartDelimiter the start block comment delimiter;
- * never {@code null} or empty
- * @param blockCommentEndDelimiter the end block comment delimiter;
- * never {@code null} or empty
- * @param statements the list that will contain the individual statements
- * @throws ScriptException if an error occurred while splitting the SQL script
+ * Execute the given SQL script.
+ *
Statement separators and comments will be removed before executing
+ * individual statements within the supplied script.
+ *
Warning: this method does not release the
+ * provided {@link Connection}.
+ * @param connection the JDBC connection to use to execute the script; already
+ * configured and ready to use
+ * @param resource the resource (potentially associated with a specific encoding)
+ * to load the SQL script from
+ * @param continueOnError whether or not to continue without throwing an exception
+ * in the event of an error
+ * @param ignoreFailedDrops whether or not to continue in the event of specifically
+ * an error on a {@code DROP} statement
+ * @param commentPrefixes the prefixes that identify single-line comments in the
+ * SQL script (typically "--")
+ * @param separator the script statement separator; defaults to
+ * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to
+ * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
+ * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a
+ * single statement without a separator
+ * @param blockCommentStartDelimiter the start block comment delimiter
+ * @param blockCommentEndDelimiter the end block comment delimiter
+ * @throws ScriptException if an error occurred while executing the SQL script
* @since 5.2
+ * @see #DEFAULT_STATEMENT_SEPARATOR
+ * @see #FALLBACK_STATEMENT_SEPARATOR
+ * @see #EOF_STATEMENT_SEPARATOR
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
+ * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
*/
- public static void splitSqlScript(@Nullable EncodedResource resource, String script,
- String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
- String blockCommentEndDelimiter, List statements) throws ScriptException {
-
- Assert.hasText(script, "'script' must not be null or empty");
- Assert.notNull(separator, "'separator' must not be null");
- Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty");
- for (String commentPrefix : commentPrefixes) {
- Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
- }
- Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
- Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
+ public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
+ boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator,
+ String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
- StringBuilder sb = new StringBuilder();
- boolean inSingleQuote = false;
- boolean inDoubleQuote = false;
- boolean inEscape = false;
+ try {
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executing SQL script from " + resource);
+ }
+ long startTime = System.currentTimeMillis();
- for (int i = 0; i < script.length(); i++) {
- char c = script.charAt(i);
- if (inEscape) {
- inEscape = false;
- sb.append(c);
- continue;
+ String script;
+ try {
+ script = readScript(resource, separator, commentPrefixes, blockCommentEndDelimiter);
}
- // MySQL style escapes
- if (c == '\\') {
- inEscape = true;
- sb.append(c);
- continue;
+ catch (IOException ex) {
+ throw new CannotReadScriptException(resource, ex);
}
- if (!inDoubleQuote && (c == '\'')) {
- inSingleQuote = !inSingleQuote;
+
+ if (separator == null) {
+ separator = DEFAULT_STATEMENT_SEPARATOR;
}
- else if (!inSingleQuote && (c == '"')) {
- inDoubleQuote = !inDoubleQuote;
+ if (!EOF_STATEMENT_SEPARATOR.equals(separator) &&
+ !containsStatementSeparator(resource, script, separator, commentPrefixes,
+ blockCommentStartDelimiter, blockCommentEndDelimiter)) {
+ separator = FALLBACK_STATEMENT_SEPARATOR;
}
- if (!inSingleQuote && !inDoubleQuote) {
- if (script.startsWith(separator, i)) {
- // We've reached the end of the current statement
- if (sb.length() > 0) {
- statements.add(sb.toString());
- sb = new StringBuilder();
- }
- i += separator.length() - 1;
- continue;
- }
- else if (startsWithAny(script, commentPrefixes, i)) {
- // Skip over any content from the start of the comment to the EOL
- int indexOfNextNewline = script.indexOf('\n', i);
- if (indexOfNextNewline > i) {
- i = indexOfNextNewline;
- continue;
+
+ List statements = new ArrayList<>();
+ splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter,
+ blockCommentEndDelimiter, statements);
+
+ int stmtNumber = 0;
+ Statement stmt = connection.createStatement();
+ try {
+ for (String statement : statements) {
+ stmtNumber++;
+ try {
+ stmt.execute(statement);
+ int rowsAffected = stmt.getUpdateCount();
+ if (logger.isDebugEnabled()) {
+ logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
+ SQLWarning warningToLog = stmt.getWarnings();
+ while (warningToLog != null) {
+ logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
+ "', error code '" + warningToLog.getErrorCode() +
+ "', message [" + warningToLog.getMessage() + "]");
+ warningToLog = warningToLog.getNextWarning();
+ }
+ }
}
- else {
- // If there's no EOL, we must be at the end of the script, so stop here.
- break;
+ catch (SQLException ex) {
+ boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
+ if (continueOnError || (dropStatement && ignoreFailedDrops)) {
+ if (logger.isDebugEnabled()) {
+ logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex);
+ }
+ }
+ else {
+ throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
+ }
}
}
- else if (script.startsWith(blockCommentStartDelimiter, i)) {
- // Skip over any block comments
- int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
- if (indexOfCommentEnd > i) {
- i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
- continue;
- }
- else {
- throw new ScriptParseException(
- "Missing block comment end delimiter: " + blockCommentEndDelimiter, resource);
- }
+ }
+ finally {
+ try {
+ stmt.close();
}
- else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') {
- // Avoid multiple adjacent whitespace characters
- if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
- c = ' ';
- }
- else {
- continue;
- }
+ catch (Throwable ex) {
+ logger.trace("Could not close JDBC Statement", ex);
}
}
- sb.append(c);
- }
- if (StringUtils.hasText(sb)) {
- statements.add(sb.toString());
+ long elapsedTime = System.currentTimeMillis() - startTime;
+ if (logger.isDebugEnabled()) {
+ logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
+ }
+ }
+ catch (Exception ex) {
+ if (ex instanceof ScriptException) {
+ throw (ScriptException) ex;
+ }
+ throw new UncategorizedScriptException(
+ "Failed to execute database script from resource [" + resource + "]", ex);
}
- }
-
- /**
- * Read a script from the given resource, using "{@code --}" as the comment prefix
- * and "{@code ;}" as the statement separator, and build a String containing the lines.
- * @param resource the {@code EncodedResource} to be read
- * @return {@code String} containing the script lines
- * @throws IOException in case of I/O errors
- */
- static String readScript(EncodedResource resource) throws IOException {
- return readScript(resource, DEFAULT_COMMENT_PREFIXES, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
}
/**
@@ -315,15 +314,15 @@ static String readScript(EncodedResource resource) throws IOException {
* within a statement — will be included in the results.
* @param resource the {@code EncodedResource} containing the script
* to be processed
+ * @param separator the statement separator in the SQL script (typically ";")
* @param commentPrefixes the prefixes that identify comments in the SQL script
* (typically "--")
- * @param separator the statement separator in the SQL script (typically ";")
* @param blockCommentEndDelimiter the end block comment delimiter
* @return a {@code String} containing the script lines
* @throws IOException in case of I/O errors
*/
- private static String readScript(EncodedResource resource, @Nullable String[] commentPrefixes,
- @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
+ static String readScript(EncodedResource resource, @Nullable String separator,
+ String[] commentPrefixes, String blockCommentEndDelimiter) throws IOException {
try (LineNumberReader lnr = new LineNumberReader(resource.getReader())) {
return readScript(lnr, commentPrefixes, separator, blockCommentEndDelimiter);
@@ -339,18 +338,21 @@ private static String readScript(EncodedResource resource, @Nullable String[] co
* a statement — will be included in the results.
* @param lineNumberReader the {@code LineNumberReader} containing the script
* to be processed
- * @param lineCommentPrefix the prefix that identifies comments in the SQL script
+ * @param commentPrefix the prefix that identifies comments in the SQL script
* (typically "--")
* @param separator the statement separator in the SQL script (typically ";")
* @param blockCommentEndDelimiter the end block comment delimiter
* @return a {@code String} containing the script lines
* @throws IOException in case of I/O errors
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
*/
- public static String readScript(LineNumberReader lineNumberReader, @Nullable String lineCommentPrefix,
+ @Deprecated
+ public static String readScript(LineNumberReader lineNumberReader, @Nullable String commentPrefix,
@Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
- String[] lineCommentPrefixes = (lineCommentPrefix != null) ? new String[] { lineCommentPrefix } : null;
- return readScript(lineNumberReader, lineCommentPrefixes, separator, blockCommentEndDelimiter);
+ String[] commentPrefixes = (commentPrefix != null) ? new String[] { commentPrefix } : null;
+ return readScript(lineNumberReader, commentPrefixes, separator, blockCommentEndDelimiter);
}
/**
@@ -362,22 +364,25 @@ public static String readScript(LineNumberReader lineNumberReader, @Nullable Str
* within a statement — will be included in the results.
* @param lineNumberReader the {@code LineNumberReader} containing the script
* to be processed
- * @param lineCommentPrefixes the prefixes that identify comments in the SQL script
+ * @param commentPrefixes the prefixes that identify comments in the SQL script
* (typically "--")
* @param separator the statement separator in the SQL script (typically ";")
* @param blockCommentEndDelimiter the end block comment delimiter
* @return a {@code String} containing the script lines
* @throws IOException in case of I/O errors
* @since 5.2
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
*/
- public static String readScript(LineNumberReader lineNumberReader, @Nullable String[] lineCommentPrefixes,
+ @Deprecated
+ public static String readScript(LineNumberReader lineNumberReader, @Nullable String[] commentPrefixes,
@Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException {
String currentStatement = lineNumberReader.readLine();
StringBuilder scriptBuilder = new StringBuilder();
while (currentStatement != null) {
if ((blockCommentEndDelimiter != null && currentStatement.contains(blockCommentEndDelimiter)) ||
- (lineCommentPrefixes != null && !startsWithAny(currentStatement, lineCommentPrefixes, 0))) {
+ (commentPrefixes != null && !startsWithAny(currentStatement, commentPrefixes, 0))) {
if (scriptBuilder.length() > 0) {
scriptBuilder.append('\n');
}
@@ -404,22 +409,54 @@ private static void appendSeparatorToScriptIfNecessary(StringBuilder scriptBuild
}
}
- private static boolean startsWithAny(String script, String[] prefixes, int offset) {
- for (String prefix : prefixes) {
- if (script.startsWith(prefix, offset)) {
- return true;
- }
- }
- return false;
+ /**
+ * Determine if the provided SQL script contains the specified delimiter.
+ * This method is intended to be used to find the string delimiting each
+ * SQL statement — for example, a ';' character.
+ *
Any occurrence of the delimiter within the script will be ignored if it
+ * is within a literal block of text enclosed in single quotes
+ * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash
+ * ({@code \}), or if it is within a single-line comment or block comment.
+ * @param script the SQL script to search within
+ * @param delimiter the statement delimiter to search for
+ * @see #DEFAULT_COMMENT_PREFIXES
+ * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
+ * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
+ */
+ @Deprecated
+ public static boolean containsSqlScriptDelimiters(String script, String delimiter) {
+ return containsStatementSeparator(null, script, delimiter, DEFAULT_COMMENT_PREFIXES,
+ DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
}
/**
- * Does the provided SQL script contain the specified delimiter?
- * @param script the SQL script
- * @param delim the string delimiting each statement - typically a ';' character
+ * Determine if the provided SQL script contains the specified statement separator.
+ *
This method is intended to be used to find the string separating each
+ * SQL statement — for example, a ';' character.
+ *
Any occurrence of the separator within the script will be ignored if it
+ * is within a literal block of text enclosed in single quotes
+ * ({@code '}) or double quotes ({@code "}), if it is escaped with a backslash
+ * ({@code \}), or if it is within a single-line comment or block comment.
+ * @param resource the resource from which the script was read, or {@code null}
+ * if unknown
+ * @param script the SQL script to search within
+ * @param separator the statement separator to search for
+ * @param commentPrefixes the prefixes that identify single-line comments
+ * (typically {@code "--"})
+ * @param blockCommentStartDelimiter the start block comment delimiter
+ * (typically {@code "/*"})
+ * @param blockCommentEndDelimiter the end block comment delimiter
+ * (typically "*/"
)
+ * @since 5.2.16
*/
- public static boolean containsSqlScriptDelimiters(String script, String delim) {
- boolean inLiteral = false;
+ private static boolean containsStatementSeparator(@Nullable EncodedResource resource, String script,
+ String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
+ String blockCommentEndDelimiter) throws ScriptException {
+
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
boolean inEscape = false;
for (int i = 0; i < script.length(); i++) {
@@ -433,11 +470,40 @@ public static boolean containsSqlScriptDelimiters(String script, String delim) {
inEscape = true;
continue;
}
- if (c == '\'') {
- inLiteral = !inLiteral;
+ if (!inDoubleQuote && (c == '\'')) {
+ inSingleQuote = !inSingleQuote;
}
- if (!inLiteral && script.startsWith(delim, i)) {
- return true;
+ else if (!inSingleQuote && (c == '"')) {
+ inDoubleQuote = !inDoubleQuote;
+ }
+ if (!inSingleQuote && !inDoubleQuote) {
+ if (script.startsWith(separator, i)) {
+ return true;
+ }
+ else if (startsWithAny(script, commentPrefixes, i)) {
+ // Skip over any content from the start of the comment to the EOL
+ int indexOfNextNewline = script.indexOf('\n', i);
+ if (indexOfNextNewline > i) {
+ i = indexOfNextNewline;
+ continue;
+ }
+ else {
+ // If there's no EOL, we must be at the end of the script, so stop here.
+ break;
+ }
+ }
+ else if (script.startsWith(blockCommentStartDelimiter, i)) {
+ // Skip over any block comments
+ int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
+ if (indexOfCommentEnd > i) {
+ i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
+ continue;
+ }
+ else {
+ throw new ScriptParseException(
+ "Missing block comment end delimiter: " + blockCommentEndDelimiter, resource);
+ }
+ }
}
}
@@ -445,206 +511,219 @@ public static boolean containsSqlScriptDelimiters(String script, String delim) {
}
/**
- * Execute the given SQL script using default settings for statement
- * separators, comment delimiters, and exception handling flags.
- *
Statement separators and comments will be removed before executing
- * individual statements within the supplied script.
- *
Warning: this method does not release the
- * provided {@link Connection}.
- * @param connection the JDBC connection to use to execute the script; already
- * configured and ready to use
- * @param resource the resource to load the SQL script from; encoded with the
- * current platform's default encoding
- * @throws ScriptException if an error occurred while executing the SQL script
- * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)
- * @see #DEFAULT_STATEMENT_SEPARATOR
- * @see #DEFAULT_COMMENT_PREFIX
- * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
- * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
- * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
- * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
+ * Split an SQL script into separate statements delimited by the provided
+ * separator character. Each individual statement will be added to the
+ * provided {@code List}.
+ *
Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the
+ * comment prefix; any text beginning with the comment prefix and extending to
+ * the end of the line will be omitted from the output. Similarly,
+ * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and
+ * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the
+ * start and end block comment delimiters: any text enclosed
+ * in a block comment will be omitted from the output. In addition, multiple
+ * adjacent whitespace characters will be collapsed into a single space.
+ * @param script the SQL script
+ * @param separator character separating each statement (typically a ';')
+ * @param statements the list that will contain the individual statements
+ * @throws ScriptException if an error occurred while splitting the SQL script
+ * @see #splitSqlScript(String, String, List)
+ * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List)
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
*/
- public static void executeSqlScript(Connection connection, Resource resource) throws ScriptException {
- executeSqlScript(connection, new EncodedResource(resource));
+ @Deprecated
+ public static void splitSqlScript(String script, char separator, List statements) throws ScriptException {
+ splitSqlScript(script, String.valueOf(separator), statements);
}
/**
- * Execute the given SQL script using default settings for statement
- * separators, comment delimiters, and exception handling flags.
- * Statement separators and comments will be removed before executing
- * individual statements within the supplied script.
- *
Warning: this method does not release the
- * provided {@link Connection}.
- * @param connection the JDBC connection to use to execute the script; already
- * configured and ready to use
- * @param resource the resource (potentially associated with a specific encoding)
- * to load the SQL script from
- * @throws ScriptException if an error occurred while executing the SQL script
- * @see #executeSqlScript(Connection, EncodedResource, boolean, boolean, String, String, String, String)
- * @see #DEFAULT_STATEMENT_SEPARATOR
- * @see #DEFAULT_COMMENT_PREFIX
- * @see #DEFAULT_BLOCK_COMMENT_START_DELIMITER
- * @see #DEFAULT_BLOCK_COMMENT_END_DELIMITER
- * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
- * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
+ * Split an SQL script into separate statements delimited by the provided
+ * separator string. Each individual statement will be added to the
+ * provided {@code List}.
+ *
Within the script, {@value #DEFAULT_COMMENT_PREFIX} will be used as the
+ * comment prefix; any text beginning with the comment prefix and extending to
+ * the end of the line will be omitted from the output. Similarly,
+ * {@value #DEFAULT_BLOCK_COMMENT_START_DELIMITER} and
+ * {@value #DEFAULT_BLOCK_COMMENT_END_DELIMITER} will be used as the
+ * start and end block comment delimiters: any text enclosed
+ * in a block comment will be omitted from the output. In addition, multiple
+ * adjacent whitespace characters will be collapsed into a single space.
+ * @param script the SQL script
+ * @param separator text separating each statement
+ * (typically a ';' or newline character)
+ * @param statements the list that will contain the individual statements
+ * @throws ScriptException if an error occurred while splitting the SQL script
+ * @see #splitSqlScript(String, char, List)
+ * @see #splitSqlScript(EncodedResource, String, String, String, String, String, List)
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
*/
- public static void executeSqlScript(Connection connection, EncodedResource resource) throws ScriptException {
- executeSqlScript(connection, resource, false, false, DEFAULT_COMMENT_PREFIX, DEFAULT_STATEMENT_SEPARATOR,
- DEFAULT_BLOCK_COMMENT_START_DELIMITER, DEFAULT_BLOCK_COMMENT_END_DELIMITER);
+ @Deprecated
+ public static void splitSqlScript(String script, String separator, List statements) throws ScriptException {
+ splitSqlScript(null, script, separator, DEFAULT_COMMENT_PREFIX, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
+ DEFAULT_BLOCK_COMMENT_END_DELIMITER, statements);
}
/**
- * Execute the given SQL script.
- * Statement separators and comments will be removed before executing
- * individual statements within the supplied script.
- *
Warning: this method does not release the
- * provided {@link Connection}.
- * @param connection the JDBC connection to use to execute the script; already
- * configured and ready to use
- * @param resource the resource (potentially associated with a specific encoding)
- * to load the SQL script from
- * @param continueOnError whether or not to continue without throwing an exception
- * in the event of an error
- * @param ignoreFailedDrops whether or not to continue in the event of specifically
- * an error on a {@code DROP} statement
- * @param commentPrefix the prefix that identifies single-line comments in the
- * SQL script (typically "--")
- * @param separator the script statement separator; defaults to
- * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to
- * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
- * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a
- * single statement without a separator
- * @param blockCommentStartDelimiter the start block comment delimiter
- * @param blockCommentEndDelimiter the end block comment delimiter
- * @throws ScriptException if an error occurred while executing the SQL script
- * @see #DEFAULT_STATEMENT_SEPARATOR
- * @see #FALLBACK_STATEMENT_SEPARATOR
- * @see #EOF_STATEMENT_SEPARATOR
- * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
- * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
+ * Split an SQL script into separate statements delimited by the provided
+ * separator string. Each individual statement will be added to the provided
+ * {@code List}.
+ *
Within the script, the provided {@code commentPrefix} will be honored:
+ * any text beginning with the comment prefix and extending to the end of the
+ * line will be omitted from the output. Similarly, the provided
+ * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
+ * delimiters will be honored: any text enclosed in a block comment will be
+ * omitted from the output. In addition, multiple adjacent whitespace characters
+ * will be collapsed into a single space.
+ * @param resource the resource from which the script was read
+ * @param script the SQL script
+ * @param separator text separating each statement
+ * (typically a ';' or newline character)
+ * @param commentPrefix the prefix that identifies SQL line comments
+ * (typically "--")
+ * @param blockCommentStartDelimiter the start block comment delimiter;
+ * never {@code null} or empty
+ * @param blockCommentEndDelimiter the end block comment delimiter;
+ * never {@code null} or empty
+ * @param statements the list that will contain the individual statements
+ * @throws ScriptException if an error occurred while splitting the SQL script
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
*/
- public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
- boolean ignoreFailedDrops, String commentPrefix, @Nullable String separator,
- String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
+ @Deprecated
+ public static void splitSqlScript(@Nullable EncodedResource resource, String script,
+ String separator, String commentPrefix, String blockCommentStartDelimiter,
+ String blockCommentEndDelimiter, List statements) throws ScriptException {
- executeSqlScript(connection, resource, continueOnError, ignoreFailedDrops,
- new String[] { commentPrefix }, separator, blockCommentStartDelimiter,
- blockCommentEndDelimiter);
+ Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
+ splitSqlScript(resource, script, separator, new String[] { commentPrefix },
+ blockCommentStartDelimiter, blockCommentEndDelimiter, statements);
}
/**
- * Execute the given SQL script.
- * Statement separators and comments will be removed before executing
- * individual statements within the supplied script.
- *
Warning: this method does not release the
- * provided {@link Connection}.
- * @param connection the JDBC connection to use to execute the script; already
- * configured and ready to use
- * @param resource the resource (potentially associated with a specific encoding)
- * to load the SQL script from
- * @param continueOnError whether or not to continue without throwing an exception
- * in the event of an error
- * @param ignoreFailedDrops whether or not to continue in the event of specifically
- * an error on a {@code DROP} statement
- * @param commentPrefixes the prefixes that identify single-line comments in the
- * SQL script (typically "--")
- * @param separator the script statement separator; defaults to
- * {@value #DEFAULT_STATEMENT_SEPARATOR} if not specified and falls back to
- * {@value #FALLBACK_STATEMENT_SEPARATOR} as a last resort; may be set to
- * {@value #EOF_STATEMENT_SEPARATOR} to signal that the script contains a
- * single statement without a separator
- * @param blockCommentStartDelimiter the start block comment delimiter
- * @param blockCommentEndDelimiter the end block comment delimiter
- * @throws ScriptException if an error occurred while executing the SQL script
+ * Split an SQL script into separate statements delimited by the provided
+ * separator string. Each individual statement will be added to the provided
+ * {@code List}.
+ *
Within the script, the provided {@code commentPrefixes} will be honored:
+ * any text beginning with one of the comment prefixes and extending to the
+ * end of the line will be omitted from the output. Similarly, the provided
+ * {@code blockCommentStartDelimiter} and {@code blockCommentEndDelimiter}
+ * delimiters will be honored: any text enclosed in a block comment will be
+ * omitted from the output. In addition, multiple adjacent whitespace characters
+ * will be collapsed into a single space.
+ * @param resource the resource from which the script was read
+ * @param script the SQL script
+ * @param separator text separating each statement
+ * (typically a ';' or newline character)
+ * @param commentPrefixes the prefixes that identify SQL line comments
+ * (typically "--")
+ * @param blockCommentStartDelimiter the start block comment delimiter;
+ * never {@code null} or empty
+ * @param blockCommentEndDelimiter the end block comment delimiter;
+ * never {@code null} or empty
+ * @param statements the list that will contain the individual statements
+ * @throws ScriptException if an error occurred while splitting the SQL script
* @since 5.2
- * @see #DEFAULT_STATEMENT_SEPARATOR
- * @see #FALLBACK_STATEMENT_SEPARATOR
- * @see #EOF_STATEMENT_SEPARATOR
- * @see org.springframework.jdbc.datasource.DataSourceUtils#getConnection
- * @see org.springframework.jdbc.datasource.DataSourceUtils#releaseConnection
+ * @deprecated as of Spring Framework 5.2.16 with no plans for replacement.
+ * This is an internal API and will likely be removed in Spring Framework 6.0.
*/
- public static void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError,
- boolean ignoreFailedDrops, String[] commentPrefixes, @Nullable String separator,
- String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {
+ @Deprecated
+ public static void splitSqlScript(@Nullable EncodedResource resource, String script,
+ String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
+ String blockCommentEndDelimiter, List statements) throws ScriptException {
- try {
- if (logger.isDebugEnabled()) {
- logger.debug("Executing SQL script from " + resource);
- }
- long startTime = System.currentTimeMillis();
+ Assert.hasText(script, "'script' must not be null or empty");
+ Assert.notNull(separator, "'separator' must not be null");
+ Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty");
+ for (String commentPrefix : commentPrefixes) {
+ Assert.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
+ }
+ Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
+ Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
- String script;
- try {
- script = readScript(resource, commentPrefixes, separator, blockCommentEndDelimiter);
+ StringBuilder sb = new StringBuilder();
+ boolean inSingleQuote = false;
+ boolean inDoubleQuote = false;
+ boolean inEscape = false;
+
+ for (int i = 0; i < script.length(); i++) {
+ char c = script.charAt(i);
+ if (inEscape) {
+ inEscape = false;
+ sb.append(c);
+ continue;
}
- catch (IOException ex) {
- throw new CannotReadScriptException(resource, ex);
+ // MySQL style escapes
+ if (c == '\\') {
+ inEscape = true;
+ sb.append(c);
+ continue;
}
-
- if (separator == null) {
- separator = DEFAULT_STATEMENT_SEPARATOR;
+ if (!inDoubleQuote && (c == '\'')) {
+ inSingleQuote = !inSingleQuote;
}
- if (!EOF_STATEMENT_SEPARATOR.equals(separator) && !containsSqlScriptDelimiters(script, separator)) {
- separator = FALLBACK_STATEMENT_SEPARATOR;
+ else if (!inSingleQuote && (c == '"')) {
+ inDoubleQuote = !inDoubleQuote;
}
-
- List statements = new ArrayList<>();
- splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter,
- blockCommentEndDelimiter, statements);
-
- int stmtNumber = 0;
- Statement stmt = connection.createStatement();
- try {
- for (String statement : statements) {
- stmtNumber++;
- try {
- stmt.execute(statement);
- int rowsAffected = stmt.getUpdateCount();
- if (logger.isDebugEnabled()) {
- logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
- SQLWarning warningToLog = stmt.getWarnings();
- while (warningToLog != null) {
- logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
- "', error code '" + warningToLog.getErrorCode() +
- "', message [" + warningToLog.getMessage() + "]");
- warningToLog = warningToLog.getNextWarning();
- }
- }
+ if (!inSingleQuote && !inDoubleQuote) {
+ if (script.startsWith(separator, i)) {
+ // We've reached the end of the current statement
+ if (sb.length() > 0) {
+ statements.add(sb.toString());
+ sb = new StringBuilder();
}
- catch (SQLException ex) {
- boolean dropStatement = StringUtils.startsWithIgnoreCase(statement.trim(), "drop");
- if (continueOnError || (dropStatement && ignoreFailedDrops)) {
- if (logger.isDebugEnabled()) {
- logger.debug(ScriptStatementFailedException.buildErrorMessage(statement, stmtNumber, resource), ex);
- }
- }
- else {
- throw new ScriptStatementFailedException(statement, stmtNumber, resource, ex);
- }
+ i += separator.length() - 1;
+ continue;
+ }
+ else if (startsWithAny(script, commentPrefixes, i)) {
+ // Skip over any content from the start of the comment to the EOL
+ int indexOfNextNewline = script.indexOf('\n', i);
+ if (indexOfNextNewline > i) {
+ i = indexOfNextNewline;
+ continue;
+ }
+ else {
+ // If there's no EOL, we must be at the end of the script, so stop here.
+ break;
}
}
- }
- finally {
- try {
- stmt.close();
+ else if (script.startsWith(blockCommentStartDelimiter, i)) {
+ // Skip over any block comments
+ int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
+ if (indexOfCommentEnd > i) {
+ i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
+ continue;
+ }
+ else {
+ throw new ScriptParseException(
+ "Missing block comment end delimiter: " + blockCommentEndDelimiter, resource);
+ }
}
- catch (Throwable ex) {
- logger.trace("Could not close JDBC Statement", ex);
+ else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') {
+ // Avoid multiple adjacent whitespace characters
+ if (sb.length() > 0 && sb.charAt(sb.length() - 1) != ' ') {
+ c = ' ';
+ }
+ else {
+ continue;
+ }
}
}
+ sb.append(c);
+ }
- long elapsedTime = System.currentTimeMillis() - startTime;
- if (logger.isDebugEnabled()) {
- logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
- }
+ if (StringUtils.hasText(sb)) {
+ statements.add(sb.toString());
}
- catch (Exception ex) {
- if (ex instanceof ScriptException) {
- throw (ScriptException) ex;
+ }
+
+ private static boolean startsWithAny(String script, String[] prefixes, int offset) {
+ for (String prefix : prefixes) {
+ if (script.startsWith(prefix, offset)) {
+ return true;
}
- throw new UncategorizedScriptException(
- "Failed to execute database script from resource [" + resource + "]", ex);
}
+ return false;
}
}
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java
index 48a0e52a899..43342ab3299 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplateTests.java
@@ -526,10 +526,11 @@ public void testBatchUpdateWithInClause() throws Exception {
@Test
public void testBatchUpdateWithSqlParameterSourcePlusTypeInfo() throws Exception {
- SqlParameterSource[] ids = new SqlParameterSource[2];
- ids[0] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC);
- ids[1] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC);
- final int[] rowsAffected = new int[] {1, 2};
+ SqlParameterSource[] ids = new SqlParameterSource[3];
+ ids[0] = new MapSqlParameterSource().addValue("id", null, Types.NULL);
+ ids[1] = new MapSqlParameterSource().addValue("id", 100, Types.NUMERIC);
+ ids[2] = new MapSqlParameterSource().addValue("id", 200, Types.NUMERIC);
+ final int[] rowsAffected = new int[] {1, 2, 3};
given(preparedStatement.executeBatch()).willReturn(rowsAffected);
given(connection.getMetaData()).willReturn(databaseMetaData);
@@ -537,13 +538,15 @@ public void testBatchUpdateWithSqlParameterSourcePlusTypeInfo() throws Exception
int[] actualRowsAffected = namedParameterTemplate.batchUpdate(
"UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = :id", ids);
- assertThat(actualRowsAffected.length == 2).as("executed 2 updates").isTrue();
+ assertThat(actualRowsAffected.length == 3).as("executed 3 updates").isTrue();
assertThat(actualRowsAffected[0]).isEqualTo(rowsAffected[0]);
assertThat(actualRowsAffected[1]).isEqualTo(rowsAffected[1]);
+ assertThat(actualRowsAffected[2]).isEqualTo(rowsAffected[2]);
verify(connection).prepareStatement("UPDATE NOSUCHTABLE SET DATE_DISPATCHED = SYSDATE WHERE ID = ?");
+ verify(preparedStatement).setNull(1, Types.NULL);
verify(preparedStatement).setObject(1, 100, Types.NUMERIC);
verify(preparedStatement).setObject(1, 200, Types.NUMERIC);
- verify(preparedStatement, times(2)).addBatch();
+ verify(preparedStatement, times(3)).addBatch();
verify(preparedStatement, atLeastOnce()).close();
verify(connection, atLeastOnce()).close();
}
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java
index 685d2f3aaeb..7a1df2928a5 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/namedparam/NamedParameterUtilsTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -23,6 +23,7 @@
import org.junit.jupiter.api.Test;
import org.springframework.dao.InvalidDataAccessApiUsageException;
+import org.springframework.jdbc.core.SqlParameterValue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -32,6 +33,7 @@
* @author Juergen Hoeller
* @author Rick Evans
* @author Artur Geraschenko
+ * @author Yanming Zhou
*/
public class NamedParameterUtilsTests {
@@ -96,6 +98,20 @@ public void convertTypeMapToArray() {
.buildSqlTypeArray(NamedParameterUtils.parseSqlStatement("xxx :a :b :c xx :a :b"), namedParams)[4]).isEqualTo(2);
}
+ @Test
+ public void convertSqlParameterValueToArray() {
+ SqlParameterValue sqlParameterValue = new SqlParameterValue(2, "b");
+ Map paramMap = new HashMap<>();
+ paramMap.put("a", "a");
+ paramMap.put("b", sqlParameterValue);
+ paramMap.put("c", "c");
+ assertThat(NamedParameterUtils.buildValueArray("xxx :a :b :c xx :a :b", paramMap)[4]).isSameAs(sqlParameterValue);
+ MapSqlParameterSource namedParams = new MapSqlParameterSource();
+ namedParams.addValue("a", "a", 1).addValue("b", sqlParameterValue).addValue("c", "c", 3);
+ assertThat(NamedParameterUtils
+ .buildValueArray(NamedParameterUtils.parseSqlStatement("xxx :a :b :c xx :a :b"), namedParams, null)[4]).isSameAs(sqlParameterValue);
+ }
+
@Test
public void convertTypeMapToSqlParameterList() {
MapSqlParameterSource namedParams = new MapSqlParameterSource();
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java
index 51a9bca7db9..42df4595207 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -46,31 +46,28 @@
*
* @author Thomas Risberg
* @author Kiril Nugmanov
+ * @author Sam Brannen
*/
-public class SimpleJdbcCallTests {
+class SimpleJdbcCallTests {
- private Connection connection;
+ private final Connection connection = mock(Connection.class);
- private DatabaseMetaData databaseMetaData;
+ private final DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
- private DataSource dataSource;
+ private final DataSource dataSource = mock(DataSource.class);
- private CallableStatement callableStatement;
+ private final CallableStatement callableStatement = mock(CallableStatement.class);
@BeforeEach
- public void setUp() throws Exception {
- connection = mock(Connection.class);
- databaseMetaData = mock(DatabaseMetaData.class);
- dataSource = mock(DataSource.class);
- callableStatement = mock(CallableStatement.class);
+ void setUp() throws Exception {
given(connection.getMetaData()).willReturn(databaseMetaData);
given(dataSource.getConnection()).willReturn(connection);
}
@Test
- public void testNoSuchStoredProcedure() throws Exception {
+ void noSuchStoredProcedure() throws Exception {
final String NO_SUCH_PROC = "x";
SQLException sqlException = new SQLException("Syntax error or access violation exception", "42000");
given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB");
@@ -81,8 +78,8 @@ public void testNoSuchStoredProcedure() throws Exception {
given(connection.prepareCall("{call " + NO_SUCH_PROC + "()}")).willReturn(callableStatement);
SimpleJdbcCall sproc = new SimpleJdbcCall(dataSource).withProcedureName(NO_SUCH_PROC);
try {
- assertThatExceptionOfType(BadSqlGrammarException.class).isThrownBy(() ->
- sproc.execute())
+ assertThatExceptionOfType(BadSqlGrammarException.class)
+ .isThrownBy(() -> sproc.execute())
.withCause(sqlException);
}
finally {
@@ -92,7 +89,7 @@ public void testNoSuchStoredProcedure() throws Exception {
}
@Test
- public void testUnnamedParameterHandling() throws Exception {
+ void unnamedParameterHandling() throws Exception {
final String MY_PROC = "my_proc";
SimpleJdbcCall sproc = new SimpleJdbcCall(dataSource).withProcedureName(MY_PROC);
// Shouldn't succeed in adding unnamed parameter
@@ -101,7 +98,7 @@ public void testUnnamedParameterHandling() throws Exception {
}
@Test
- public void testAddInvoiceProcWithoutMetaDataUsingMapParamSource() throws Exception {
+ void addInvoiceProcWithoutMetaDataUsingMapParamSource() throws Exception {
initializeAddInvoiceWithoutMetaData(false);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice");
adder.declareParameters(
@@ -117,7 +114,7 @@ public void testAddInvoiceProcWithoutMetaDataUsingMapParamSource() throws Except
}
@Test
- public void testAddInvoiceProcWithoutMetaDataUsingArrayParams() throws Exception {
+ void addInvoiceProcWithoutMetaDataUsingArrayParams() throws Exception {
initializeAddInvoiceWithoutMetaData(false);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice");
adder.declareParameters(
@@ -131,7 +128,7 @@ public void testAddInvoiceProcWithoutMetaDataUsingArrayParams() throws Exception
}
@Test
- public void testAddInvoiceProcWithMetaDataUsingMapParamSource() throws Exception {
+ void addInvoiceProcWithMetaDataUsingMapParamSource() throws Exception {
initializeAddInvoiceWithMetaData(false);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice");
Number newId = adder.executeObject(Number.class, new MapSqlParameterSource()
@@ -143,7 +140,7 @@ public void testAddInvoiceProcWithMetaDataUsingMapParamSource() throws Exception
}
@Test
- public void testAddInvoiceProcWithMetaDataUsingArrayParams() throws Exception {
+ void addInvoiceProcWithMetaDataUsingArrayParams() throws Exception {
initializeAddInvoiceWithMetaData(false);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withProcedureName("add_invoice");
Number newId = adder.executeObject(Number.class, 1103, 3);
@@ -153,7 +150,7 @@ public void testAddInvoiceProcWithMetaDataUsingArrayParams() throws Exception {
}
@Test
- public void testAddInvoiceFuncWithoutMetaDataUsingMapParamSource() throws Exception {
+ void addInvoiceFuncWithoutMetaDataUsingMapParamSource() throws Exception {
initializeAddInvoiceWithoutMetaData(true);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice");
adder.declareParameters(
@@ -169,7 +166,7 @@ public void testAddInvoiceFuncWithoutMetaDataUsingMapParamSource() throws Except
}
@Test
- public void testAddInvoiceFuncWithoutMetaDataUsingArrayParams() throws Exception {
+ void addInvoiceFuncWithoutMetaDataUsingArrayParams() throws Exception {
initializeAddInvoiceWithoutMetaData(true);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice");
adder.declareParameters(
@@ -183,7 +180,7 @@ public void testAddInvoiceFuncWithoutMetaDataUsingArrayParams() throws Exception
}
@Test
- public void testAddInvoiceFuncWithMetaDataUsingMapParamSource() throws Exception {
+ void addInvoiceFuncWithMetaDataUsingMapParamSource() throws Exception {
initializeAddInvoiceWithMetaData(true);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice");
Number newId = adder.executeFunction(Number.class, new MapSqlParameterSource()
@@ -192,22 +189,20 @@ public void testAddInvoiceFuncWithMetaDataUsingMapParamSource() throws Exception
assertThat(newId.intValue()).isEqualTo(4);
verifyAddInvoiceWithMetaData(true);
verify(connection, atLeastOnce()).close();
-
}
@Test
- public void testAddInvoiceFuncWithMetaDataUsingArrayParams() throws Exception {
+ void addInvoiceFuncWithMetaDataUsingArrayParams() throws Exception {
initializeAddInvoiceWithMetaData(true);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice");
Number newId = adder.executeFunction(Number.class, 1103, 3);
assertThat(newId.intValue()).isEqualTo(4);
verifyAddInvoiceWithMetaData(true);
verify(connection, atLeastOnce()).close();
-
}
@Test
- public void testCorrectFunctionStatement() throws Exception {
+ void correctFunctionStatement() throws Exception {
initializeAddInvoiceWithMetaData(true);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withFunctionName("add_invoice");
adder.compile();
@@ -215,7 +210,7 @@ public void testCorrectFunctionStatement() throws Exception {
}
@Test
- public void testCorrectFunctionStatementNamed() throws Exception {
+ void correctFunctionStatementNamed() throws Exception {
initializeAddInvoiceWithMetaData(true);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withNamedBinding().withFunctionName("add_invoice");
adder.compile();
@@ -223,13 +218,51 @@ public void testCorrectFunctionStatementNamed() throws Exception {
}
@Test
- public void testCorrectProcedureStatementNamed() throws Exception {
+ void correctProcedureStatementNamed() throws Exception {
initializeAddInvoiceWithMetaData(false);
SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withNamedBinding().withProcedureName("add_invoice");
adder.compile();
verifyStatement(adder, "{call ADD_INVOICE(AMOUNT => ?, CUSTID => ?, NEWID => ?)}");
}
+ /**
+ * This test demonstrates that a CALL statement will still be generated if
+ * an exception occurs while retrieving metadata, potentially resulting in
+ * missing metadata and consequently a failure while invoking the stored
+ * procedure.
+ */
+ @Test // gh-26486
+ void exceptionThrownWhileRetrievingColumnNamesFromMetadata() throws Exception {
+ ResultSet proceduresResultSet = mock(ResultSet.class);
+ ResultSet procedureColumnsResultSet = mock(ResultSet.class);
+
+ given(databaseMetaData.getDatabaseProductName()).willReturn("Oracle");
+ given(databaseMetaData.getUserName()).willReturn("ME");
+ given(databaseMetaData.storesUpperCaseIdentifiers()).willReturn(true);
+ given(databaseMetaData.getProcedures("", "ME", "ADD_INVOICE")).willReturn(proceduresResultSet);
+ given(databaseMetaData.getProcedureColumns("", "ME", "ADD_INVOICE", null)).willReturn(procedureColumnsResultSet);
+
+ given(proceduresResultSet.next()).willReturn(true, false);
+ given(proceduresResultSet.getString("PROCEDURE_NAME")).willReturn("add_invoice");
+
+ given(procedureColumnsResultSet.next()).willReturn(true, true, true, false);
+ given(procedureColumnsResultSet.getString("COLUMN_NAME")).willReturn("amount", "custid", "newid");
+ given(procedureColumnsResultSet.getInt("DATA_TYPE"))
+ // Return a valid data type for the first 2 columns.
+ .willReturn(Types.INTEGER, Types.INTEGER)
+ // 3rd time, simulate an error while retrieving metadata.
+ .willThrow(new SQLException("error with DATA_TYPE for column 3"));
+
+ SimpleJdbcCall adder = new SimpleJdbcCall(dataSource).withNamedBinding().withProcedureName("add_invoice");
+ adder.compile();
+ // If an exception were not thrown for column 3, we would expect:
+ // {call ADD_INVOICE(AMOUNT => ?, CUSTID => ?, NEWID => ?)}
+ verifyStatement(adder, "{call ADD_INVOICE(AMOUNT => ?, CUSTID => ?)}");
+
+ verify(proceduresResultSet).close();
+ verify(procedureColumnsResultSet).close();
+ }
+
private void verifyStatement(SimpleJdbcCall adder, String expected) {
assertThat(adder.getCallString()).as("Incorrect call statement").isEqualTo(expected);
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java
index ec399d3c281..d965d598f01 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcInsertTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -19,7 +19,9 @@
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
-import java.util.HashMap;
+import java.sql.SQLException;
+import java.sql.Types;
+import java.util.Collections;
import javax.sql.DataSource;
@@ -29,45 +31,44 @@
import org.springframework.dao.InvalidDataAccessApiUsageException;
+import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
/**
- * Mock object based tests for SimpleJdbcInsert.
+ * Mock object based tests for {@link SimpleJdbcInsert}.
*
* @author Thomas Risberg
+ * @author Sam Brannen
*/
-public class SimpleJdbcInsertTests {
+class SimpleJdbcInsertTests {
- private Connection connection;
+ private final Connection connection = mock(Connection.class);
- private DatabaseMetaData databaseMetaData;
+ private final DatabaseMetaData databaseMetaData = mock(DatabaseMetaData.class);
- private DataSource dataSource;
+ private final DataSource dataSource = mock(DataSource.class);
@BeforeEach
- public void setUp() throws Exception {
- connection = mock(Connection.class);
- databaseMetaData = mock(DatabaseMetaData.class);
- dataSource = mock(DataSource.class);
+ void setUp() throws Exception {
given(connection.getMetaData()).willReturn(databaseMetaData);
given(dataSource.getConnection()).willReturn(connection);
}
@AfterEach
- public void verifyClosed() throws Exception {
+ void verifyClosed() throws Exception {
verify(connection).close();
}
@Test
- public void testNoSuchTable() throws Exception {
+ void noSuchTable() throws Exception {
ResultSet resultSet = mock(ResultSet.class);
given(resultSet.next()).willReturn(false);
- given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB");
+
given(databaseMetaData.getDatabaseProductName()).willReturn("MyDB");
given(databaseMetaData.getDatabaseProductVersion()).willReturn("1.0");
given(databaseMetaData.getUserName()).willReturn("me");
@@ -76,9 +77,65 @@ public void testNoSuchTable() throws Exception {
SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource).withTableName("x");
// Shouldn't succeed in inserting into table which doesn't exist
- assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
- insert.execute(new HashMap<>()));
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(() -> insert.execute(Collections.emptyMap()))
+ .withMessageStartingWith("Unable to locate columns for table 'x' so an insert statement can't be generated");
+
verify(resultSet).close();
}
+ @Test // gh-26486
+ void retrieveColumnNamesFromMetadata() throws Exception {
+ ResultSet tableResultSet = mock(ResultSet.class);
+ given(tableResultSet.next()).willReturn(true, false);
+
+ given(databaseMetaData.getUserName()).willReturn("me");
+ given(databaseMetaData.getTables(null, null, "me", null)).willReturn(tableResultSet);
+
+ ResultSet columnResultSet = mock(ResultSet.class);
+ given(databaseMetaData.getColumns(null, "me", null, null)).willReturn(columnResultSet);
+ given(columnResultSet.next()).willReturn(true, true, false);
+ given(columnResultSet.getString("COLUMN_NAME")).willReturn("col1", "col2");
+ given(columnResultSet.getInt("DATA_TYPE")).willReturn(Types.VARCHAR);
+ given(columnResultSet.getBoolean("NULLABLE")).willReturn(false);
+
+ SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource).withTableName("me");
+ insert.compile();
+ assertThat(insert.getInsertString()).isEqualTo("INSERT INTO me (col1, col2) VALUES(?, ?)");
+
+ verify(columnResultSet).close();
+ verify(tableResultSet).close();
+ }
+
+ @Test // gh-26486
+ void exceptionThrownWhileRetrievingColumnNamesFromMetadata() throws Exception {
+ ResultSet tableResultSet = mock(ResultSet.class);
+ given(tableResultSet.next()).willReturn(true, false);
+
+ given(databaseMetaData.getUserName()).willReturn("me");
+ given(databaseMetaData.getTables(null, null, "me", null)).willReturn(tableResultSet);
+
+ ResultSet columnResultSet = mock(ResultSet.class);
+ given(databaseMetaData.getColumns(null, "me", null, null)).willReturn(columnResultSet);
+ // true, true, false --> simulates processing of two columns
+ given(columnResultSet.next()).willReturn(true, true, false);
+ given(columnResultSet.getString("COLUMN_NAME"))
+ // Return a column name the first time.
+ .willReturn("col1")
+ // Second time, simulate an error while retrieving metadata.
+ .willThrow(new SQLException("error with col2"));
+ given(columnResultSet.getInt("DATA_TYPE")).willReturn(Types.VARCHAR);
+ given(columnResultSet.getBoolean("NULLABLE")).willReturn(false);
+
+ SimpleJdbcInsert insert = new SimpleJdbcInsert(dataSource).withTableName("me");
+
+ assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
+ .isThrownBy(insert::compile)
+ .withMessage("Unable to locate columns for table 'me' so an insert statement can't be generated. " +
+ "Consider specifying explicit column names -- for example, via SimpleJdbcInsert#usingColumns().");
+
+ verify(columnResultSet).close();
+ verify(tableResultSet).close();
+ }
+
}
diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java
index f57adece236..e033ef03391 100644
--- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java
+++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsUnitTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -20,6 +20,8 @@
import java.util.List;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.support.EncodedResource;
@@ -46,6 +48,7 @@
public class ScriptUtilsUnitTests {
@Test
+ @SuppressWarnings("deprecation")
public void splitSqlScriptDelimitedWithSemicolon() {
String rawStatement1 = "insert into customer (id, name)\nvalues (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')";
String cleanedStatement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')";
@@ -61,6 +64,7 @@ public void splitSqlScriptDelimitedWithSemicolon() {
}
@Test
+ @SuppressWarnings("deprecation")
public void splitSqlScriptDelimitedWithNewLine() {
String statement1 = "insert into customer (id, name) values (1, 'Rod ; Johnson'), (2, 'Adrian \n Collier')";
String statement2 = "insert into orders(id, order_date, customer_id) values (1, '2008-01-02', 2)";
@@ -73,6 +77,7 @@ public void splitSqlScriptDelimitedWithNewLine() {
}
@Test
+ @SuppressWarnings("deprecation")
public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() {
String statement1 = "do something";
String statement2 = "do something else";
@@ -84,6 +89,7 @@ public void splitSqlScriptDelimitedWithNewLineButDefaultDelimiterSpecified() {
}
@Test // SPR-13218
+ @SuppressWarnings("deprecation")
public void splitScriptWithSingleQuotesNestedInsideDoubleQuotes() throws Exception {
String statement1 = "select '1' as \"Dogbert's owner's\" from dual";
String statement2 = "select '2' as \"Dilbert's\" from dual";
@@ -95,6 +101,7 @@ public void splitScriptWithSingleQuotesNestedInsideDoubleQuotes() throws Excepti
}
@Test // SPR-11560
+ @SuppressWarnings("deprecation")
public void readAndSplitScriptWithMultipleNewlinesAsSeparator() throws Exception {
String script = readScript("db-test-data-multi-newline.sql");
List statements = new ArrayList<>();
@@ -122,6 +129,7 @@ public void readAndSplitScriptContainingCommentsWithMultiplePrefixes() throws Ex
splitScriptContainingComments(script, "--", "#", "^");
}
+ @SuppressWarnings("deprecation")
private void splitScriptContainingComments(String script, String... commentPrefixes) throws Exception {
List statements = new ArrayList<>();
splitSqlScript(null, script, ";", commentPrefixes, DEFAULT_BLOCK_COMMENT_START_DELIMITER,
@@ -135,6 +143,7 @@ private void splitScriptContainingComments(String script, String... commentPrefi
}
@Test // SPR-10330
+ @SuppressWarnings("deprecation")
public void readAndSplitScriptContainingCommentsWithLeadingTabs() throws Exception {
String script = readScript("test-data-with-comments-and-leading-tabs.sql");
List statements = new ArrayList<>();
@@ -146,6 +155,7 @@ public void readAndSplitScriptContainingCommentsWithLeadingTabs() throws Excepti
}
@Test // SPR-9531
+ @SuppressWarnings("deprecation")
public void readAndSplitScriptContainingMultiLineComments() throws Exception {
String script = readScript("test-data-with-multi-line-comments.sql");
List statements = new ArrayList<>();
@@ -156,6 +166,7 @@ public void readAndSplitScriptContainingMultiLineComments() throws Exception {
}
@Test
+ @SuppressWarnings("deprecation")
public void readAndSplitScriptContainingMultiLineNestedComments() throws Exception {
String script = readScript("test-data-with-multi-line-nested-comments.sql");
List statements = new ArrayList<>();
@@ -165,22 +176,48 @@ public void readAndSplitScriptContainingMultiLineNestedComments() throws Excepti
assertThat(statements).containsExactly(statement1, statement2);
}
- @Test
- public void containsDelimiters() {
- assertThat(containsSqlScriptDelimiters("select 1\n select ';'", ";")).isFalse();
- assertThat(containsSqlScriptDelimiters("select 1; select 2", ";")).isTrue();
- assertThat(containsSqlScriptDelimiters("select 1; select '\\n\n';", "\n")).isFalse();
- assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n")).isTrue();
- assertThat(containsSqlScriptDelimiters("select 1\n select 2", "\n\n")).isFalse();
- assertThat(containsSqlScriptDelimiters("select 1\n\n select 2", "\n\n")).isTrue();
- // MySQL style escapes '\\'
- assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('a\\\\', 'b;')", ";")).isFalse();
- assertThat(containsSqlScriptDelimiters("insert into users(first_name, last_name)\nvalues('Charles', 'd\\'Artagnan'); select 1;", ";")).isTrue();
+ @ParameterizedTest
+ @CsvSource(delimiter = '#', value = {
+ // semicolon
+ "'select 1\n select '';''' # ; # false",
+ "'select 1\n select \";\"' # ; # false",
+ "'select 1; select 2' # ; # true",
+ // newline
+ "'select 1; select ''\n''' # '\n' # false",
+ "'select 1; select \"\n\"' # '\n' # false",
+ "'select 1\n select 2' # '\n' # true",
+ // double newline
+ "'select 1\n select 2' # '\n\n' # false",
+ "'select 1\n\n select 2' # '\n\n' # true",
+ // semicolon with MySQL style escapes '\\'
+ "'insert into users(first, last)\nvalues(''a\\\\'', ''b;'')' # ; # false",
+ "'insert into users(first, last)\nvalues(''Charles'', ''d\\''Artagnan''); select 1' # ; # true",
+ // semicolon inside comments
+ "'-- a;b;c\ninsert into colors(color_num) values(42);' # ; # true",
+ "'/* a;b;c */\ninsert into colors(color_num) values(42);' # ; # true",
+ "'-- a;b;c\ninsert into colors(color_num) values(42)' # ; # false",
+ "'/* a;b;c */\ninsert into colors(color_num) values(42)' # ; # false",
+ // single quotes inside comments
+ "'-- What\\''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true",
+ "'-- What''s your favorite color?\ninsert into colors(color_num) values(42);' # ; # true",
+ "'/* What\\''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true",
+ "'/* What''s your favorite color? */\ninsert into colors(color_num) values(42);' # ; # true",
+ // double quotes inside comments
+ "'-- double \" quotes\ninsert into colors(color_num) values(42);' # ; # true",
+ "'-- double \\\" quotes\ninsert into colors(color_num) values(42);' # ; # true",
+ "'/* double \" quotes */\ninsert into colors(color_num) values(42);' # ; # true",
+ "'/* double \\\" quotes */\ninsert into colors(color_num) values(42);' # ; # true"
+ })
+ @SuppressWarnings("deprecation")
+ public void containsStatementSeparator(String script, String delimiter, boolean expected) {
+ // Indirectly tests ScriptUtils.containsStatementSeparator(EncodedResource, String, String, String[], String, String).
+ assertThat(containsSqlScriptDelimiters(script, delimiter)).isEqualTo(expected);
}
private String readScript(String path) throws Exception {
EncodedResource resource = new EncodedResource(new ClassPathResource(path, getClass()));
- return ScriptUtils.readScript(resource);
+ return ScriptUtils.readScript(resource, DEFAULT_STATEMENT_SEPARATOR, DEFAULT_COMMENT_PREFIXES,
+ DEFAULT_BLOCK_COMMENT_END_DELIMITER);
}
}
diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql
index 7faa91c250a..5df377ccade 100644
--- a/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql
+++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/datasource/init/test-data-with-multi-line-nested-comments.sql
@@ -5,16 +5,19 @@
* x, y, z...
*/
+-- This is a single line comment containing single (') and double quotes (").
INSERT INTO users(first_name, last_name) VALUES('Juergen', 'Hoeller');
-- This is also a comment.
/*-------------------------------------------
--- A fancy multi-line comments that puts
+-- A fancy multi-line comment that puts
-- single line comments inside of a multi-line
-- comment block.
Moreover, the block comment end delimiter
appears on a line that can potentially also
be a single-line comment if we weren't
already inside a multi-line comment run.
+
+And here's a line containing single and double quotes (").
-------------------------------------------*/
INSERT INTO
users(first_name, last_name) -- This is a single line comment containing the block-end-comment sequence here */ but it's still a single-line comment
diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java
index a36c29bb8da..df38ee48209 100644
--- a/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java
+++ b/spring-jms/src/main/java/org/springframework/jms/listener/DefaultMessageListenerContainer.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -198,9 +198,9 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe
private int registeredWithDestination = 0;
- private volatile boolean recovering = false;
+ private volatile boolean recovering;
- private volatile boolean interrupted = false;
+ private volatile boolean interrupted;
@Nullable
private Runnable stopCallback;
@@ -963,11 +963,8 @@ protected void refreshConnectionUntilSuccessful() {
}
}
if (!applyBackOffTime(execution)) {
- StringBuilder msg = new StringBuilder();
- msg.append("Stopping container for destination '")
- .append(getDestinationDescription())
- .append("': back-off policy does not allow ").append("for further attempts.");
- logger.error(msg.toString());
+ logger.error("Stopping container for destination '" + getDestinationDescription() +
+ "': back-off policy does not allow for further attempts.");
stop();
}
}
diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java
index ec5d566222f..43b9a1a6ca5 100644
--- a/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java
+++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -69,12 +69,12 @@ public class JmsNamespaceHandlerTests {
@BeforeEach
- public void setUp() throws Exception {
+ public void setup() {
this.context = new ToolingTestApplicationContext("jmsNamespaceHandlerTests.xml", getClass());
}
@AfterEach
- public void tearDown() throws Exception {
+ public void shutdown() {
this.context.close();
}
@@ -87,13 +87,12 @@ public void testBeansCreated() {
containers = context.getBeansOfType(GenericMessageEndpointManager.class);
assertThat(containers.size()).as("Context should contain 3 JCA endpoint containers").isEqualTo(3);
- Map containerFactories =
- context.getBeansOfType(JmsListenerContainerFactory.class);
- assertThat(containerFactories.size()).as("Context should contain 3 JmsListenerContainerFactory instances").isEqualTo(3);
+ assertThat(context.getBeansOfType(JmsListenerContainerFactory.class))
+ .as("Context should contain 3 JmsListenerContainerFactory instances").hasSize(3);
}
@Test
- public void testContainerConfiguration() throws Exception {
+ public void testContainerConfiguration() {
Map containers = context.getBeansOfType(DefaultMessageListenerContainer.class);
ConnectionFactory defaultConnectionFactory = context.getBean(DEFAULT_CONNECTION_FACTORY, ConnectionFactory.class);
ConnectionFactory explicitConnectionFactory = context.getBean(EXPLICIT_CONNECTION_FACTORY, ConnectionFactory.class);
@@ -115,7 +114,7 @@ else if (container.getConnectionFactory().equals(explicitConnectionFactory)) {
}
@Test
- public void testJcaContainerConfiguration() throws Exception {
+ public void testJcaContainerConfiguration() {
Map containers = context.getBeansOfType(JmsMessageEndpointManager.class);
assertThat(containers.containsKey("listener3")).as("listener3 not found").isTrue();
diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java
index 6f8ca80c4fe..76bd754e2cc 100644
--- a/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java
+++ b/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -75,8 +75,7 @@ public void testSettingMessageListenerToAnUnsupportedType() {
@Test
public void testSessionTransactedModeReallyDoesDefaultToFalse() {
assertThat(this.container.isPubSubNoLocal()).as("The [pubSubLocal] property of SimpleMessageListenerContainer " +
- "must default to false. Change this test (and the " +
- "attendant Javadoc) if you have changed the default.").isFalse();
+ "must default to false. Change this test (and the attendant javadoc) if you have changed the default.").isFalse();
}
@Test
@@ -121,6 +120,7 @@ public void testContextRefreshedEventDoesNotStartTheConnectionIfAutoStartIsSetTo
GenericApplicationContext context = new GenericApplicationContext();
context.getBeanFactory().registerSingleton("messageListenerContainer", this.container);
context.refresh();
+ context.close();
verify(connection).setExceptionListener(this.container);
}
@@ -151,6 +151,7 @@ public void testContextRefreshedEventStartsTheConnectionByDefault() throws Excep
GenericApplicationContext context = new GenericApplicationContext();
context.getBeanFactory().registerSingleton("messageListenerContainer", this.container);
context.refresh();
+ context.close();
verify(connection).setExceptionListener(this.container);
verify(connection).start();
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java
index c3feebc1a09..b3da10249df 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java
@@ -120,8 +120,7 @@ public void setCustomArgumentResolvers(List custo
* the ones configured by default. This is an advanced option. For most use cases
* it should be sufficient to use {@link #setCustomArgumentResolvers(java.util.List)}.
*/
- @SuppressWarnings("ConstantConditions")
- public void setArgumentResolvers(List argumentResolvers) {
+ public void setArgumentResolvers(@Nullable List argumentResolvers) {
if (argumentResolvers == null) {
this.argumentResolvers.clear();
return;
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java
index 7ae1c1bf9ca..4865e3bf6f1 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java
@@ -131,7 +131,6 @@ protected String getToStringInfix() {
* @param message the current message
* @return the frame type or {@code null} if not found
*/
- @SuppressWarnings("ConstantConditions")
@Nullable
public static FrameType getFrameType(Message> message) {
return (FrameType) message.getHeaders().get(RSocketFrameTypeMessageCondition.FRAME_TYPE_HEADER);
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java
index f4f8ebe9000..37c2d3b4002 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -42,10 +42,16 @@ public abstract class AbstractBrokerRegistration {
private final List destinationPrefixes;
+ /**
+ * Create a new broker registration.
+ * @param clientInboundChannel the inbound channel
+ * @param clientOutboundChannel the outbound channel
+ * @param destinationPrefixes the destination prefixes
+ */
public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel,
MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) {
- Assert.notNull(clientOutboundChannel, "'clientInboundChannel' must not be null");
+ Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null");
Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null");
this.clientInboundChannel = clientInboundChannel;
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java
index 4c11e684552..68e60f691b5 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -40,8 +40,16 @@ public class SimpleBrokerRegistration extends AbstractBrokerRegistration {
private String selectorHeaderName = "selector";
- public SimpleBrokerRegistration(SubscribableChannel inChannel, MessageChannel outChannel, String[] prefixes) {
- super(inChannel, outChannel, prefixes);
+ /**
+ * Create a new {@code SimpleBrokerRegistration}.
+ * @param clientInboundChannel the inbound channel
+ * @param clientOutboundChannel the outbound channel
+ * @param destinationPrefixes the destination prefixes
+ */
+ public SimpleBrokerRegistration(SubscribableChannel clientInboundChannel,
+ MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
+
+ super(clientInboundChannel, clientOutboundChannel, destinationPrefixes);
}
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java
index 8db37b7e977..02f29d466d8 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -64,6 +64,12 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
private String userRegistryBroadcast;
+ /**
+ * Create a new {@code StompBrokerRelayRegistration}.
+ * @param clientInboundChannel the inbound channel
+ * @param clientOutboundChannel the outbound channel
+ * @param destinationPrefixes the destination prefixes
+ */
public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel,
MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
@@ -233,7 +239,6 @@ protected String getUserRegistryBroadcast() {
@Override
protected StompBrokerRelayMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
-
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(
getClientInboundChannel(), getClientOutboundChannel(),
brokerChannel, getDestinationPrefixes());
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java
index ef7130f54a9..95e04b63f48 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageHeaderAccessor.java
@@ -378,13 +378,14 @@ private List getMatchingHeaderNames(String pattern, @Nullable Map headersToCopy) {
- if (headersToCopy != null) {
- headersToCopy.forEach((key, value) -> {
- if (!isReadOnly(key)) {
- setHeader(key, value);
- }
- });
+ if (headersToCopy == null || this.headers == headersToCopy) {
+ return;
}
+ headersToCopy.forEach((key, value) -> {
+ if (!isReadOnly(key)) {
+ setHeader(key, value);
+ }
+ });
}
/**
@@ -392,13 +393,14 @@ public void copyHeaders(@Nullable Map headersToCopy) {
* This operation will not overwrite any existing values.
*/
public void copyHeadersIfAbsent(@Nullable Map headersToCopy) {
- if (headersToCopy != null) {
- headersToCopy.forEach((key, value) -> {
- if (!isReadOnly(key)) {
- setHeaderIfAbsent(key, value);
- }
- });
+ if (headersToCopy == null || this.headers == headersToCopy) {
+ return;
}
+ headersToCopy.forEach((key, value) -> {
+ if (!isReadOnly(key)) {
+ setHeaderIfAbsent(key, value);
+ }
+ });
}
protected boolean isReadOnly(String headerName) {
diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java
index fbd70f3598d..1ccded25c8c 100644
--- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java
+++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java
@@ -16,6 +16,7 @@
package org.springframework.messaging.support;
+import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@@ -75,6 +76,8 @@ protected NativeMessageHeaderAccessor(@Nullable Message> message) {
@SuppressWarnings("unchecked")
Map> map = (Map>) getHeader(NATIVE_HEADERS);
if (map != null) {
+ // setHeader checks for equality but we need copy of native headers
+ setHeader(NATIVE_HEADERS, null);
setHeader(NATIVE_HEADERS, new LinkedMultiValueMap<>(map));
}
}
@@ -103,6 +106,8 @@ public void setImmutable() {
if (isMutable()) {
Map> map = getNativeHeaders();
if (map != null) {
+ // setHeader checks for equality but we need immutable wrapper
+ setHeader(NATIVE_HEADERS, null);
setHeader(NATIVE_HEADERS, Collections.unmodifiableMap(map));
}
super.setImmutable();
@@ -110,31 +115,34 @@ public void setImmutable() {
}
@Override
- public void setHeader(String name, @Nullable Object value) {
- if (name.equalsIgnoreCase(NATIVE_HEADERS)) {
- // Force removal since setHeader checks for equality
- super.setHeader(NATIVE_HEADERS, null);
+ public void copyHeaders(@Nullable Map headersToCopy) {
+ if (headersToCopy == null) {
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ Map> map = (Map>) headersToCopy.get(NATIVE_HEADERS);
+ if (map != null && map != getNativeHeaders()) {
+ map.forEach(this::setNativeHeaderValues);
}
- super.setHeader(name, value);
+
+ // setHeader checks for equality, native headers should be equal by now
+ super.copyHeaders(headersToCopy);
}
@Override
- @SuppressWarnings("unchecked")
- public void copyHeaders(@Nullable Map headersToCopy) {
- if (headersToCopy != null) {
- Map> nativeHeaders = getNativeHeaders();
- Map> map = (Map>) headersToCopy.get(NATIVE_HEADERS);
- if (map != null) {
- if (nativeHeaders != null) {
- nativeHeaders.putAll(map);
- }
- else {
- nativeHeaders = new LinkedMultiValueMap<>(map);
- }
- }
- super.copyHeaders(headersToCopy);
- setHeader(NATIVE_HEADERS, nativeHeaders);
+ public void copyHeadersIfAbsent(@Nullable Map headersToCopy) {
+ if (headersToCopy == null) {
+ return;
+ }
+
+ @SuppressWarnings("unchecked")
+ Map> map = (Map>) headersToCopy.get(NATIVE_HEADERS);
+ if (map != null && getNativeHeaders() == null) {
+ map.forEach(this::setNativeHeaderValues);
}
+
+ super.copyHeadersIfAbsent(headersToCopy);
}
/**
@@ -201,6 +209,30 @@ public void setNativeHeader(String name, @Nullable String value) {
}
}
+ /**
+ * Variant of {@link #addNativeHeader(String, String)} for all values.
+ * @since 5.2.12
+ */
+ public void setNativeHeaderValues(String name, @Nullable List values) {
+ Assert.state(isMutable(), "Already immutable");
+ Map> map = getNativeHeaders();
+ if (values == null) {
+ if (map != null && map.get(name) != null) {
+ setModified(true);
+ map.remove(name);
+ }
+ return;
+ }
+ if (map == null) {
+ map = new LinkedMultiValueMap<>(3);
+ setHeader(NATIVE_HEADERS, map);
+ }
+ if (!ObjectUtils.nullSafeEquals(values, getHeader(name))) {
+ setModified(true);
+ map.put(name, new ArrayList<>(values));
+ }
+ }
+
/**
* Add the specified native header value to existing values.
* In order for this to work, the accessor must be {@link #isMutable()
diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java
index 4bf79b57d2d..90985f35d79 100644
--- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java
+++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -58,7 +58,7 @@ public class ReactorNettyTcpStompClientTests {
@BeforeEach
- public void setUp(TestInfo testInfo) throws Exception {
+ public void setup(TestInfo testInfo) throws Exception {
logger.debug("Setting up before '" + testInfo.getTestMethod().get().getName() + "'");
int port = SocketUtils.findAvailableTcpPort(61613);
@@ -81,7 +81,7 @@ public void setUp(TestInfo testInfo) throws Exception {
}
@AfterEach
- public void tearDown() throws Exception {
+ public void shutdown() throws Exception {
try {
this.client.shutdown();
}
diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java
index 609f12f7beb..c3c0d029346 100644
--- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java
+++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandlerIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -75,7 +75,7 @@ public class StompBrokerRelayMessageHandlerIntegrationTests {
@BeforeEach
- public void setUp(TestInfo testInfo) throws Exception {
+ public void setup(TestInfo testInfo) throws Exception {
logger.debug("Setting up before '" + testInfo.getTestMethod().get().getName() + "'");
this.port = SocketUtils.findAvailableTcpPort(61613);
@@ -83,11 +83,11 @@ public void setUp(TestInfo testInfo) throws Exception {
this.responseHandler = new TestMessageHandler();
this.responseChannel.subscribe(this.responseHandler);
this.eventPublisher = new TestEventPublisher();
- startActiveMqBroker();
+ startActiveMQBroker();
createAndStartRelay();
}
- private void startActiveMqBroker() throws Exception {
+ private void startActiveMQBroker() throws Exception {
this.activeMQBroker = new BrokerService();
this.activeMQBroker.addConnector("stomp://localhost:" + this.port);
this.activeMQBroker.setStartAsync(false);
@@ -217,7 +217,7 @@ public void relayReconnectsIfBrokerComesBackUp() throws Exception {
this.eventPublisher.expectBrokerAvailabilityEvent(false);
- startActiveMqBroker();
+ startActiveMQBroker();
this.eventPublisher.expectBrokerAvailabilityEvent(true);
}
@@ -274,8 +274,7 @@ public void handleMessage(Message> message) throws MessagingException {
}
public void expectMessages(MessageExchange... messageExchanges) throws InterruptedException {
- List expectedMessages =
- new ArrayList<>(Arrays.asList(messageExchanges));
+ List expectedMessages = new ArrayList<>(Arrays.asList(messageExchanges));
while (expectedMessages.size() > 0) {
Message> message = this.queue.poll(10000, TimeUnit.MILLISECONDS);
assertThat(message).as("Timed out waiting for messages, expected [" + expectedMessages + "]").isNotNull();
@@ -451,7 +450,7 @@ public StompFrameMessageMatcher(StompCommand command, String sessionId) {
@Override
public final boolean match(Message> message) {
StompHeaderAccessor headers = StompHeaderAccessor.wrap(message);
- if (!this.command.equals(headers.getCommand()) || (this.sessionId != headers.getSessionId())) {
+ if (!this.command.equals(headers.getCommand()) || !this.sessionId.equals(headers.getSessionId())) {
return false;
}
return matchInternal(headers, message.getPayload());
diff --git a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java
index 726f80c256e..73eba06caa4 100644
--- a/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java
+++ b/spring-messaging/src/test/java/org/springframework/messaging/support/NativeMessageHeaderAccessorTests.java
@@ -226,19 +226,46 @@ public void setImmutableIdempotent() {
@Test // gh-25821
void copyImmutableToMutable() {
- NativeMessageHeaderAccessor source = new NativeMessageHeaderAccessor();
- source.addNativeHeader("foo", "bar");
- Message message = MessageBuilder.createMessage("payload", source.getMessageHeaders());
+ NativeMessageHeaderAccessor sourceAccessor = new NativeMessageHeaderAccessor();
+ sourceAccessor.addNativeHeader("foo", "bar");
+ Message source = MessageBuilder.createMessage("payload", sourceAccessor.getMessageHeaders());
- NativeMessageHeaderAccessor target = new NativeMessageHeaderAccessor();
- target.copyHeaders(message.getHeaders());
- target.setLeaveMutable(true);
- message = MessageBuilder.createMessage(message.getPayload(), target.getMessageHeaders());
+ NativeMessageHeaderAccessor targetAccessor = new NativeMessageHeaderAccessor();
+ targetAccessor.copyHeaders(source.getHeaders());
+ targetAccessor.setLeaveMutable(true);
+ Message> target = MessageBuilder.createMessage(source.getPayload(), targetAccessor.getMessageHeaders());
- MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(message);
+ MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(target);
assertThat(accessor.isMutable());
((NativeMessageHeaderAccessor) accessor).addNativeHeader("foo", "baz");
assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz");
}
+ @Test // gh-25821
+ void copyIfAbsentImmutableToMutable() {
+ NativeMessageHeaderAccessor sourceAccessor = new NativeMessageHeaderAccessor();
+ sourceAccessor.addNativeHeader("foo", "bar");
+ Message source = MessageBuilder.createMessage("payload", sourceAccessor.getMessageHeaders());
+
+ MessageHeaderAccessor targetAccessor = new NativeMessageHeaderAccessor();
+ targetAccessor.copyHeadersIfAbsent(source.getHeaders());
+ targetAccessor.setLeaveMutable(true);
+ Message> target = MessageBuilder.createMessage(source.getPayload(), targetAccessor.getMessageHeaders());
+
+ MessageHeaderAccessor accessor = MessageHeaderAccessor.getMutableAccessor(target);
+ assertThat(accessor.isMutable());
+ ((NativeMessageHeaderAccessor) accessor).addNativeHeader("foo", "baz");
+ assertThat(((NativeMessageHeaderAccessor) accessor).getNativeHeader("foo")).containsExactly("bar", "baz");
+ }
+
+ @Test // gh-26155
+ void copySelf() {
+ NativeMessageHeaderAccessor accessor = new NativeMessageHeaderAccessor();
+ accessor.addNativeHeader("foo", "bar");
+ accessor.setHeader("otherHeader", "otherHeaderValue");
+ accessor.setLeaveMutable(true);
+
+ // Does not fail with ConcurrentModificationException
+ accessor.copyHeaders(accessor.getMessageHeaders());
+ }
}
diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java
index 84b42e3747c..f6dc0869793 100644
--- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java
+++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-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.
@@ -193,8 +193,8 @@ public String getPersistenceUnitName() {
* {@code Persistence.createEntityManagerFactory} (if any).
* Can be populated with a String "value" (parsed via PropertiesEditor) or a
* "props" element in XML bean definitions.
- * @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map)
- * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map)
+ * @see javax.persistence.Persistence#createEntityManagerFactory(String, Map)
+ * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, Map)
*/
public void setJpaProperties(Properties jpaProperties) {
CollectionUtils.mergePropertiesIntoMap(jpaProperties, this.jpaPropertyMap);
@@ -204,8 +204,8 @@ public void setJpaProperties(Properties jpaProperties) {
* Specify JPA properties as a Map, to be passed into
* {@code Persistence.createEntityManagerFactory} (if any).
*
Can be populated with a "map" or "props" element in XML bean definitions.
- * @see javax.persistence.Persistence#createEntityManagerFactory(String, java.util.Map)
- * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(javax.persistence.spi.PersistenceUnitInfo, java.util.Map)
+ * @see javax.persistence.Persistence#createEntityManagerFactory(String, Map)
+ * @see javax.persistence.spi.PersistenceProvider#createContainerEntityManagerFactory(PersistenceUnitInfo, Map)
*/
public void setJpaPropertyMap(@Nullable Map jpaProperties) {
if (jpaProperties != null) {
@@ -400,10 +400,13 @@ private EntityManagerFactory buildNativeEntityManagerFactory() {
String message = ex.getMessage();
String causeString = cause.toString();
if (!message.endsWith(causeString)) {
- throw new PersistenceException(message + "; nested exception is " + causeString, cause);
+ ex = new PersistenceException(message + "; nested exception is " + causeString, cause);
}
}
}
+ if (logger.isErrorEnabled()) {
+ logger.error("Failed to initialize JPA EntityManagerFactory: " + ex.getMessage());
+ }
throw ex;
}
diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java
index 15c27ccc5f0..8885d6eb7e0 100644
--- a/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java
+++ b/spring-orm/src/test/java/org/springframework/orm/jpa/AbstractContainerEntityManagerFactoryIntegrationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -224,8 +224,7 @@ public void testQueryNoPersonsSharedNotTransactional() {
q.setFlushMode(FlushModeType.AUTO);
List people = q.getResultList();
assertThat(people.size()).isEqualTo(0);
- assertThatExceptionOfType(Exception.class).isThrownBy(() ->
- q.getSingleResult())
+ assertThatExceptionOfType(Exception.class).isThrownBy(q::getSingleResult)
.withMessageContaining("closed");
// We would typically expect an IllegalStateException, but Hibernate throws a
// PersistenceException. So we assert the contents of the exception message instead.
diff --git a/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java
index 881529d211b..f7b9b0e5102 100644
--- a/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java
+++ b/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -64,6 +64,8 @@
import com.thoughtworks.xstream.mapper.CannotResolveClassException;
import com.thoughtworks.xstream.mapper.Mapper;
import com.thoughtworks.xstream.mapper.MapperWrapper;
+import com.thoughtworks.xstream.security.ForbiddenClassException;
+import com.thoughtworks.xstream.security.TypePermission;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
@@ -106,7 +108,7 @@
* Therefore, it has limited namespace support. As such, it is rather unsuitable for
* usage within Web Services.
*
- * This marshaller requires XStream 1.4.5 or higher, as of Spring 4.3.
+ *
This marshaller requires XStream 1.4.7 or higher, as of Spring 5.2.17.
* Note that {@link XStream} construction has been reworked in 4.0, with the
* stream driver and the class loader getting passed into XStream itself now.
*
@@ -146,6 +148,9 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo
@Nullable
private ConverterMatcher[] converters;
+ @Nullable
+ private TypePermission[] typePermissions;
+
@Nullable
private MarshallingStrategy marshallingStrategy;
@@ -268,6 +273,20 @@ public void setConverters(ConverterMatcher... converters) {
this.converters = converters;
}
+ /**
+ * Set XStream type permissions such as
+ * {@link com.thoughtworks.xstream.security.AnyTypePermission},
+ * {@link com.thoughtworks.xstream.security.ExplicitTypePermission} etc,
+ * as an alternative to overriding the {@link #customizeXStream} method.
+ *
Note: As of XStream 1.4.18, the default type permissions are
+ * restricted to well-known core JDK types. For any custom types,
+ * explicit type permissions need to be registered.
+ * @since 5.2.17
+ */
+ public void setTypePermissions(TypePermission... typePermissions) {
+ this.typePermissions = typePermissions;
+ }
+
/**
* Set a custom XStream {@link MarshallingStrategy} to use.
* @since 4.0
@@ -407,7 +426,7 @@ public void setBeanClassLoader(ClassLoader classLoader) {
@Override
public void afterPropertiesSet() {
- // no-op due to use of SingletonSupplier for the XStream field.
+ // no-op due to use of SingletonSupplier for the XStream field
}
/**
@@ -479,6 +498,12 @@ else if (this.converters[i] instanceof SingleValueConverter) {
}
}
+ if (this.typePermissions != null) {
+ for (TypePermission permission : this.typePermissions) {
+ xstream.addPermission(permission);
+ }
+ }
+
if (this.marshallingStrategy != null) {
xstream.setMarshallingStrategy(this.marshallingStrategy);
}
@@ -844,7 +869,7 @@ private Object doUnmarshal(HierarchicalStreamReader streamReader, @Nullable Data
*/
protected XmlMappingException convertXStreamException(Exception ex, boolean marshalling) {
if (ex instanceof StreamException || ex instanceof CannotResolveClassException ||
- ex instanceof ConversionException) {
+ ex instanceof ForbiddenClassException || ex instanceof ConversionException) {
if (marshalling) {
return new MarshallingFailureException("XStream marshalling exception", ex);
}
diff --git a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java
index 5f05936d751..304d3fcc771 100644
--- a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java
+++ b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamMarshallerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -43,6 +43,7 @@
import com.thoughtworks.xstream.io.json.JettisonMappedXmlDriver;
import com.thoughtworks.xstream.io.json.JsonHierarchicalStreamDriver;
import com.thoughtworks.xstream.io.json.JsonWriter;
+import com.thoughtworks.xstream.security.AnyTypePermission;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InOrder;
@@ -67,18 +68,21 @@
/**
* @author Arjen Poutsma
* @author Sam Brannen
+ * @author Juergen Hoeller
*/
class XStreamMarshallerTests {
private static final String EXPECTED_STRING = "42";
- private final XStreamMarshaller marshaller = new XStreamMarshaller();
-
private final Flight flight = new Flight();
+ private XStreamMarshaller marshaller;
+
@BeforeEach
void createMarshaller() {
+ marshaller = new XStreamMarshaller();
+ marshaller.setTypePermissions(AnyTypePermission.ANY);
marshaller.setAliases(Collections.singletonMap("flight", Flight.class.getName()));
flight.setFlightNumber(42L);
}
@@ -143,7 +147,7 @@ void marshalStreamResultOutputStream() throws Exception {
ByteArrayOutputStream os = new ByteArrayOutputStream();
StreamResult result = new StreamResult(os);
marshaller.marshal(flight, result);
- String s = new String(os.toByteArray(), "UTF-8");
+ String s = os.toString("UTF-8");
assertThat(XmlContent.of(s)).isSimilarTo(EXPECTED_STRING);
}
diff --git a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java
index 1c864545aa6..7c87eda2253 100644
--- a/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java
+++ b/spring-oxm/src/test/java/org/springframework/oxm/xstream/XStreamUnmarshallerTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -18,6 +18,7 @@
import java.io.ByteArrayInputStream;
import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@@ -29,6 +30,7 @@
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
+import com.thoughtworks.xstream.security.AnyTypePermission;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.w3c.dom.Document;
@@ -40,6 +42,7 @@
/**
* @author Arjen Poutsma
+ * @author Juergen Hoeller
*/
public class XStreamUnmarshallerTests {
@@ -47,21 +50,16 @@ public class XStreamUnmarshallerTests {
private XStreamMarshaller unmarshaller;
+
@BeforeEach
- public void createUnmarshaller() throws Exception {
+ public void createUnmarshaller() {
unmarshaller = new XStreamMarshaller();
+ unmarshaller.setTypePermissions(AnyTypePermission.ANY);
Map> aliases = new HashMap<>();
aliases.put("flight", Flight.class);
unmarshaller.setAliases(aliases);
}
- private void testFlight(Object o) {
- boolean condition = o instanceof Flight;
- assertThat(condition).as("Unmarshalled object is not Flights").isTrue();
- Flight flight = (Flight) o;
- assertThat(flight).as("Flight is null").isNotNull();
- assertThat(flight.getFlightNumber()).as("Number is invalid").isEqualTo(42L);
- }
@Test
public void unmarshalDomSource() throws Exception {
@@ -83,7 +81,7 @@ public void unmarshalStaxSourceXmlStreamReader() throws Exception {
@Test
public void unmarshalStreamSourceInputStream() throws Exception {
- StreamSource source = new StreamSource(new ByteArrayInputStream(INPUT_STRING.getBytes("UTF-8")));
+ StreamSource source = new StreamSource(new ByteArrayInputStream(INPUT_STRING.getBytes(StandardCharsets.UTF_8)));
Object flights = unmarshaller.unmarshal(source);
testFlight(flights);
}
@@ -94,5 +92,15 @@ public void unmarshalStreamSourceReader() throws Exception {
Object flights = unmarshaller.unmarshal(source);
testFlight(flights);
}
+
+
+ private void testFlight(Object o) {
+ boolean condition = o instanceof Flight;
+ assertThat(condition).as("Unmarshalled object is not Flights").isTrue();
+ Flight flight = (Flight) o;
+ assertThat(flight).as("Flight is null").isNotNull();
+ assertThat(flight.getFlightNumber()).as("Number is invalid").isEqualTo(42L);
+ }
+
}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java
index 5f67f51177c..372898cd5a3 100644
--- a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -22,6 +22,7 @@
import javax.servlet.http.Cookie;
+import org.springframework.core.style.ToStringCreator;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -152,4 +153,22 @@ private static String extractAttributeValue(String attribute, String header) {
return nameAndValue[1];
}
+ @Override
+ public String toString() {
+ return new ToStringCreator(this)
+ .append("name", getName())
+ .append("value", getValue())
+ .append("Path", getPath())
+ .append("Domain", getDomain())
+ .append("Version", getVersion())
+ .append("Comment", getComment())
+ .append("Secure", getSecure())
+ .append("HttpOnly", isHttpOnly())
+ .append("SameSite", this.sameSite)
+ .append("Max-Age", getMaxAge())
+ .append("Expires", (this.expires != null ?
+ DateTimeFormatter.RFC_1123_DATE_TIME.format(this.expires) : null))
+ .toString();
+ }
+
}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
index f043c090030..d163da9abfa 100644
--- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java
@@ -374,10 +374,10 @@ private String getCookieHeader(Cookie cookie) {
buf.append("; Domain=").append(cookie.getDomain());
}
int maxAge = cookie.getMaxAge();
+ ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null);
if (maxAge >= 0) {
buf.append("; Max-Age=").append(maxAge);
buf.append("; Expires=");
- ZonedDateTime expires = (cookie instanceof MockCookie ? ((MockCookie) cookie).getExpires() : null);
if (expires != null) {
buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
@@ -387,6 +387,10 @@ private String getCookieHeader(Cookie cookie) {
buf.append(headers.getFirst(HttpHeaders.EXPIRES));
}
}
+ else if (expires != null) {
+ buf.append("; Expires=");
+ buf.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
+ }
if (cookie.getSecure()) {
buf.append("; Secure");
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
index 359d945a1d3..781ab7a6e48 100644
--- a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-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.
@@ -21,6 +21,7 @@
import java.io.IOException;
import java.io.InputStream;
+import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
@@ -42,10 +43,10 @@ public class MockMultipartFile implements MultipartFile {
private final String name;
- private String originalFilename;
+ private final String originalFilename;
@Nullable
- private String contentType;
+ private final String contentType;
private final byte[] content;
@@ -79,7 +80,7 @@ public MockMultipartFile(String name, InputStream contentStream) throws IOExcept
public MockMultipartFile(
String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) {
- Assert.hasLength(name, "Name must not be null");
+ Assert.hasLength(name, "Name must not be empty");
this.name = name;
this.originalFilename = (originalFilename != null ? originalFilename : "");
this.contentType = contentType;
@@ -108,6 +109,7 @@ public String getName() {
}
@Override
+ @NonNull
public String getOriginalFilename() {
return this.originalFilename;
}
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java
index 7dab1c8c21b..232faade3c3 100644
--- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java
+++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -315,8 +315,8 @@ public Set getResourcePaths(String path) {
return resourcePaths;
}
catch (InvalidPathException | IOException ex ) {
- if (logger.isWarnEnabled()) {
- logger.warn("Could not get resource paths for " +
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not get resource paths for " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
@@ -339,8 +339,8 @@ public URL getResource(String path) throws MalformedURLException {
throw ex;
}
catch (InvalidPathException | IOException ex) {
- if (logger.isWarnEnabled()) {
- logger.warn("Could not get URL for resource " +
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not get URL for resource " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
@@ -360,8 +360,8 @@ public InputStream getResourceAsStream(String path) {
return resource.getInputStream();
}
catch (InvalidPathException | IOException ex) {
- if (logger.isWarnEnabled()) {
- logger.warn("Could not open InputStream for resource " +
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not open InputStream for resource " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
@@ -476,8 +476,8 @@ public String getRealPath(String path) {
return resource.getFile().getAbsolutePath();
}
catch (InvalidPathException | IOException ex) {
- if (logger.isWarnEnabled()) {
- logger.warn("Could not determine real path of resource " +
+ if (logger.isDebugEnabled()) {
+ logger.debug("Could not determine real path of resource " +
(resource != null ? resource : resourceLocation), ex);
}
return null;
diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java
index e81bb9c8569..dfa062ceb2b 100644
--- a/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java
+++ b/spring-test/src/main/java/org/springframework/test/annotation/Repeat.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -28,7 +28,11 @@
*
* Note that the scope of execution to be repeated includes execution of the
* test method itself as well as any set up or tear down of
- * the test fixture.
+ * the test fixture. When used with the
+ * {@link org.springframework.test.context.junit4.rules.SpringMethodRule
+ * SpringMethodRule}, the scope additionally includes
+ * {@linkplain org.springframework.test.context.TestExecutionListener#prepareTestInstance
+ * preparation of the test instance}.
*
*
This annotation may be used as a meta-annotation to create custom
* composed annotations.
diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java
index a5ee5b264a9..9ad1e3068aa 100644
--- a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java
+++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2019 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.
@@ -79,7 +79,7 @@
*
The default value is {@code true}, which means that a test
* class will inherit bean definition profiles defined by a
* test superclass. Specifically, the bean definition profiles for a test
- * class will be added to the list of bean definition profiles
+ * class will be appended to the list of bean definition profiles
* defined by a test superclass. Thus, subclasses have the option of
* extending the list of bean definition profiles.
*
If {@code inheritProfiles} is set to {@code false}, the bean
diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java
index db37af2f02b..f434b14022a 100644
--- a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java
+++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2020 the original author or authors.
+ * Copyright 2002-2017 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.
@@ -19,8 +19,8 @@
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashSet;
import java.util.Set;
-import java.util.TreeSet;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
@@ -533,8 +533,8 @@ private static String[] processActiveProfiles(@Nullable String[] activeProfiles)
return EMPTY_STRING_ARRAY;
}
- // Active profiles must be unique and sorted
- Set profilesSet = new TreeSet<>(Arrays.asList(activeProfiles));
+ // Active profiles must be unique
+ Set profilesSet = new LinkedHashSet<>(Arrays.asList(activeProfiles));
return StringUtils.toStringArray(profilesSet);
}
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
index 30d67923e4f..d93b78d88d5 100644
--- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
+++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -221,8 +221,12 @@ public void beforeTestClass() throws Exception {
/**
* Hook for preparing a test instance prior to execution of any individual
- * test methods, for example for injecting dependencies, etc. Should be
- * called immediately after instantiation of the test instance.
+ * test methods — for example, to inject dependencies.
+ * This method should be called immediately after instantiation of the test
+ * class or as soon after instantiation as possible (as is the case with the
+ * {@link org.springframework.test.context.junit4.rules.SpringMethodRule
+ * SpringMethodRule}). In any case, this method must be called prior to any
+ * framework-specific lifecycle callbacks.
*
The managed {@link TestContext} will be updated with the supplied
* {@code testInstance}.
*
An attempt will be made to give each registered
diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
index 815f1940d39..0c7854f9aa0 100644
--- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
+++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -84,10 +84,14 @@ default void beforeTestClass(TestContext testContext) throws Exception {
}
/**
- * Prepares the {@link Object test instance} of the supplied
- * {@link TestContext test context}, for example by injecting dependencies.
+ * Prepares the {@linkplain Object test instance} of the supplied
+ * {@linkplain TestContext test context} — for example, to inject
+ * dependencies.
*
This method should be called immediately after instantiation of the test
- * instance but prior to any framework-specific lifecycle callbacks.
+ * class or as soon after instantiation as possible (as is the case with the
+ * {@link org.springframework.test.context.junit4.rules.SpringMethodRule
+ * SpringMethodRule}). In any case, this method must be called prior to any
+ * framework-specific lifecycle callbacks.
*
The default implementation is empty. Can be overridden by
* concrete classes as necessary.
* @param testContext the test context for the test; never {@code null}
@@ -121,8 +125,8 @@ default void beforeTestMethod(TestContext testContext) throws Exception {
/**
* Pre-processes a test immediately before execution of the
- * {@link java.lang.reflect.Method test method} in the supplied
- * {@link TestContext test context} — for example, for timing
+ * {@linkplain java.lang.reflect.Method test method} in the supplied
+ * {@linkplain TestContext test context} — for example, for timing
* or logging purposes.
*
This method must be called after framework-specific
* before lifecycle callbacks.
@@ -141,8 +145,8 @@ default void beforeTestExecution(TestContext testContext) throws Exception {
/**
* Post-processes a test immediately after execution of the
- * {@link java.lang.reflect.Method test method} in the supplied
- * {@link TestContext test context} — for example, for timing
+ * {@linkplain java.lang.reflect.Method test method} in the supplied
+ * {@linkplain TestContext test context} — for example, for timing
* or logging purposes.
*
This method must be called before framework-specific
* after lifecycle callbacks.
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java
index 8c21fe375be..cc8f3214993 100644
--- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java
+++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2017 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -34,6 +34,8 @@
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.annotation.DirtiesContext.HierarchyMode;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -105,6 +107,7 @@ protected ConditionEvaluationResult evaluateAnnotation(Cl
boolean loadContext = loadContextExtractor.apply(annotation.get());
boolean evaluatedToTrue = evaluateExpression(expression, loadContext, annotationType, context);
+ ConditionEvaluationResult result;
if (evaluatedToTrue) {
String adjective = (enabledOnTrue ? "enabled" : "disabled");
@@ -114,7 +117,7 @@ protected ConditionEvaluationResult evaluateAnnotation(Cl
if (logger.isInfoEnabled()) {
logger.info(reason);
}
- return (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
+ result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason)
: ConditionEvaluationResult.disabled(reason));
}
else {
@@ -124,9 +127,25 @@ protected ConditionEvaluationResult evaluateAnnotation(Cl
if (logger.isDebugEnabled()) {
logger.debug(reason);
}
- return (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
+ result = (enabledOnTrue ? ConditionEvaluationResult.disabled(reason) :
ConditionEvaluationResult.enabled(reason));
}
+
+ // If we eagerly loaded the ApplicationContext to evaluate SpEL expressions
+ // and the test class ends up being disabled, we have to check if the
+ // user asked for the ApplicationContext to be closed via @DirtiesContext,
+ // since the DirtiesContextTestExecutionListener will never be invoked for
+ // a disabled test class.
+ // See https://github.com/spring-projects/spring-framework/issues/26694
+ if (loadContext && result.isDisabled() && element instanceof Class) {
+ Class> testClass = (Class>) element;
+ findMergedAnnotation(testClass, DirtiesContext.class).ifPresent(dirtiesContext -> {
+ HierarchyMode hierarchyMode = dirtiesContext.hierarchyMode();
+ SpringExtension.getTestContextManager(context).getTestContext().markApplicationContextDirty(hierarchyMode);
+ });
+ }
+
+ return result;
}
private boolean evaluateExpression(String expression, boolean loadContext,
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
index 6b2b4abeeb7..860f5589aee 100644
--- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
+++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/SpringExtension.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2019 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -206,7 +206,7 @@ public static ApplicationContext getApplicationContext(ExtensionContext context)
* Get the {@link TestContextManager} associated with the supplied {@code ExtensionContext}.
* @return the {@code TestContextManager} (never {@code null})
*/
- private static TestContextManager getTestContextManager(ExtensionContext context) {
+ static TestContextManager getTestContextManager(ExtensionContext context) {
Assert.notNull(context, "ExtensionContext must not be null");
Class> testClass = context.getRequiredTestClass();
Store store = getStore(context);
diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java
index 639ee9d6f62..57478d4bd8a 100644
--- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java
+++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2018 the original author or authors.
+ * Copyright 2002-2021 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.
@@ -79,6 +79,10 @@
* NOTE: As of Spring Framework 4.3, this class requires JUnit 4.12 or higher.
*
*
WARNING: Due to the shortcomings of JUnit rules, the
+ * {@code SpringMethodRule}
+ * {@linkplain org.springframework.test.context.TestExecutionListener#prepareTestInstance
+ * prepares the test instance} before {@code @Before} lifecycle methods instead of
+ * immediately after instantiation of the test class. In addition, the
* {@code SpringMethodRule} does not support the
* {@code beforeTestExecution()} and {@code afterTestExecution()} callbacks of the
* {@link org.springframework.test.context.TestExecutionListener TestExecutionListener}
diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java
index 5e2c76582ab..491ee370279 100644
--- a/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java
+++ b/spring-test/src/main/java/org/springframework/test/context/support/ActiveProfilesUtils.java
@@ -16,8 +16,11 @@
package org.springframework.test.context.support;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
import java.util.Set;
-import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -67,7 +70,7 @@ abstract class ActiveProfilesUtils {
static String[] resolveActiveProfiles(Class> testClass) {
Assert.notNull(testClass, "Class must not be null");
- Set activeProfiles = new TreeSet<>();
+ List