Compare commits

...

29 Commits

Author SHA1 Message Date
Spring Builds 08bc1a050e Release v5.3.27 2023-04-13 08:43:55 +00:00
Sam Brannen 6bfb94a563 Disable variable assignment in SimpleEvaluationContext
This commit introduces infrastructure to differentiate between
programmatic setting of a variable in an EvaluationContext versus the
assignment of a variable within a SpEL expression using the assignment
operator (=). In addition, this commit disables variable assignment
within expressions when using the SimpleEvaluationContext.

Closes gh-30327
2023-04-13 10:20:06 +02:00
Sam Brannen ebc8265428 Limit SpEL expression length
This commit enforces a limit of the maximum size of a single SpEL
expression.

Closes gh-30329
2023-04-13 10:15:02 +02:00
Sam Brannen 86457464d7 Limit string concatenation in SpEL expressions
This commit introduces support for limiting the maximum length of a
string resulting from the concatenation operator (+) in SpEL
expressions.

Closes gh-30331
2023-04-13 10:14:18 +02:00
Sam Brannen be129dc171 Change max regex length in SpEL expressions to 1000
This commit changes the max regex length in SpEL expressions from 1024
to 1000 in order to consistently use "round" numbers for recently
introduced limits.

See gh-30265
2023-04-13 10:14:15 +02:00
Juergen Hoeller 8bb1b3eb44 Upgrade to Netty 4.1.91 and Checkstyle 10.9.3 2023-04-12 13:53:17 +02:00
Stephane Nicoll 6abd822e77 Upgrade to Reactor 2020.0.31
Closes gh-30315
2023-04-11 17:35:25 +02:00
rstoyanchev 1c43a4c7ab Fix regression in ReactorServerHttpRequest
Instead of a backport for cef916, this change simply undoes the
optimization that led to the regression.

Closes gh-30314
2023-04-11 15:12:54 +01:00
Sam Brannen 423f2215c2 Remove flaky assertion to fix build on JDK 17
Sometime between JDK 8 and JDK 17, the behavior for List::toArray()
changed. Specifically, the type returned for List<String> changed from
String[] to Object[].

This commit therefore removes an assertion against this particular
JDK-specific behavior. The affected test method retains additional
assertions along the same lines but which are not flaky.
2023-04-10 17:34:04 +02:00
Sébastien Deleuze 0bad69d5fb Fix SSE with indenting serializer in WebMvc.fn
This commit ensures that HTTP headers like "text/event-stream"
are correctly forwarded to the converter used in
SseServerResponse for proper pretty print handling.

Close gh-30302
2023-04-07 11:56:47 +02:00
Sam Brannen 6b19642256 Increase max regex length in SpEL expressions
This commit increases the max regex length in SpEL expressions from 256
to 1024 in order to support use cases where a regex may be rather long
without necessarily increasing the complexity of the regex.

Closes gh-30298
2023-04-06 18:02:36 +02:00
Sam Brannen 19bb4e96f2 Improve Javadoc for ObjectUtils.nullSafeConciseToString() 2023-04-06 17:36:32 +02:00
Sam Brannen 7a2594acda Add tests for corner cases
See gh-30290
See gh-30286
2023-04-05 15:28:45 +02:00
Sam Brannen 91c58af7af Introduce ObjectUtils.nullSafeConciseToString()
ObjectUtils.nullSafeToString(Object) exists for generating a string
representation of various objects in a "null-safe" manner, including
support for object graphs, collections, etc.

However, there are times when we would like to generate a "concise",
null-safe string representation that does not include an entire object
graph (or potentially a collection of object graphs).

This commit introduces ObjectUtils.nullSafeConciseToString(Object) to
address this need and makes use of the new feature in FieldError and
ConversionFailedException.

Closes gh-30286
2023-04-05 15:25:59 +02:00
Sam Brannen 0a1aeafe08 Introduce StringUtils.truncate()
StringUtils.truncate() serves as central, consistent way for truncating
strings used in log messages and exception failure messages, for
immediate use in LogFormatUtils and ObjectUtils.

See gh-30286
Closes gh-30290
2023-04-05 15:15:53 +02:00
Simon Baslé 0e69bac7b0 Polish 0cfbf60: fix a test for Java 8 compatibility 2023-04-04 15:25:27 +02:00
Simon Baslé 0cfbf6036c Rename MockMVC matcher methods to prevent regression in user tests
This commit changes the name of two recently introduced methods in the
`MockRestRequestMatchers` class for header and queryParam. These have
been found to cause false negatives in user tests, due to the new
overload taking precedence in some cases.

Namely, using a `Matcher` factory method which can apply to both `List`
and `String` will cause the compiler to select the newest list overload,
by instantiating a `Matcher<Object>`.

This can cause false negatives in user tests, failing tests that used
to pass because the Matcher previously applied to the first String in
the header or queryParam value list. For instance, `equalsTo("a")`.

The new overloads are recent enough and this has enough potential to
cause an arbitrary number of user tests to fail that we break the API
to eliminate the ambiguity, by renaming the methods with a `*List`
suffix.

See gh-30220
See gh-30238
Closes gh-30235
2023-04-04 15:07:41 +02:00
Sébastien Deleuze e931fdc5c2 Make HttpComponentsHeadersAdapter#getFirst nullable
Backport of gh-30267

Closes gh-30269
2023-04-03 09:06:32 +02:00
Sébastien Deleuze 3a1681357c Fix PathVariable reference documentation code snippets
Closes gh-30258
2023-03-31 15:51:45 +02:00
Juergen Hoeller a93382dbbf Propagate HttpStreamResetException itself if cause is null
Closes gh-30245

(cherry picked from commit 8fca258207)
2023-03-30 19:32:29 +02:00
Sébastien Deleuze 79c5ef88f5 Refine generic type management in AbstractMessageWriterResultHandler
This commit updates AbstractMessageWriterResultHandler#writeBody in
order to use the declared bodyParameter instead of
ResolvableType.forInstance(body) when the former has unresolvable
generics.

Closes gh-30215
2023-03-30 18:26:15 +02:00
Juergen Hoeller 0c80e5f9e6 Use JdkDynamicAopProxy class loader instead of JDK bootstrap/platform loader
Closes gh-30115

(cherry picked from commit 7e905e3e00)
2023-03-29 14:07:50 +02:00
Juergen Hoeller 7ad01a94d6 Use MethodInvocationInfo class loader in case of JDK platform loader as well
Closes gh-30210

(cherry picked from commit 491ae1e3be)
2023-03-29 13:56:23 +02:00
Juergen Hoeller 5d6d653cbd Use MethodInvocationInfo class loader in case of core JDK interface type
Closes gh-30210

(cherry picked from commit ce2689eead)
2023-03-28 13:56:02 +02:00
Johnny Lim 9868e888a5 Update versions in Javadoc
Closes gh-30191
2023-03-25 17:17:28 +01:00
ghostg00 6f6eb3d996 Fix example in Javadoc for @EnableWebSocket
The `echoWebSocketHandler()` method is not defined in the
`WebSocketConfigurer` interface and should therefore be annotated with
`@Bean` instead of `@Override`.

Closes gh-30187
2023-03-24 17:55:24 +01:00
Giuseppe a94c50e294 Handle all exceptions for stored proc out param retrieval in SharedEntityManagerCreator
Prior to this commit, the EntityManager was not closed in
SharedEntityManagerCreator.DeferredQueryInvocationHandler's
invoke(Object, Method, Object[]) method if an invocation of
getOutputParameterValue(*) threw an exception other than
IllegalArgumentException, which could lead to a connection leak.

This commit addresses this by catching RuntimeException instead of
IllegalArgumentException.

Closes gh-30164
2023-03-22 15:44:06 +01:00
Kukri b8c1255ee0 Fix anchor in link to "Web on Reactive Stack" chapter
Closes gh-30163
2023-03-22 11:35:12 +01:00
Spring Builds 129062034b Next development version (v5.3.27-SNAPSHOT) 2023-03-20 10:05:15 +00:00
50 changed files with 1059 additions and 213 deletions
+3 -3
View File
@@ -28,8 +28,8 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
mavenBom "io.netty:netty-bom:4.1.90.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.30"
mavenBom "io.netty:netty-bom:4.1.91.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.31"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR13"
mavenBom "io.rsocket:rsocket-bom:1.1.3"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.51.v20230217"
@@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project ->
}
checkstyle {
toolVersion = "10.9.1"
toolVersion = "10.9.3"
configDirectory.set(rootProject.file("src/checkstyle"))
}
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.26-SNAPSHOT
version=5.3.27
org.gradle.jvmargs=-Xmx2048m
org.gradle.caching=true
org.gradle.parallel=true
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -123,6 +123,11 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
if (classLoader == null || classLoader.getParent() == null) {
// JDK bootstrap loader or platform loader suggested ->
// use higher-level loader which can see Spring infrastructure classes
classLoader = getClass().getClassLoader();
}
return Proxy.newProxyInstance(classLoader, this.proxiedInterfaces, this);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,10 @@
package org.springframework.aop.framework;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.accessibility.Accessible;
@@ -380,6 +383,40 @@ public class ProxyFactoryTests {
assertThat(proxy.getName()).isEqualTo("tb");
}
@Test
public void testCharSequenceProxy() {
CharSequence target = "test";
ProxyFactory pf = new ProxyFactory(target);
ClassLoader cl = target.getClass().getClassLoader();
assertThat(((CharSequence) pf.getProxy(cl)).toString()).isEqualTo(target);
}
@Test
public void testDateProxy() {
Date target = new Date();
ProxyFactory pf = new ProxyFactory(target);
pf.setProxyTargetClass(true);
ClassLoader cl = target.getClass().getClassLoader();
assertThat(((Date) pf.getProxy(cl)).getTime()).isEqualTo(target.getTime());
}
@Test
public void testJdbcSavepointProxy() throws SQLException {
Savepoint target = new Savepoint() {
@Override
public int getSavepointId() throws SQLException {
return 1;
}
@Override
public String getSavepointName() throws SQLException {
return "sp";
}
};
ProxyFactory pf = new ProxyFactory(target);
ClassLoader cl = Savepoint.class.getClassLoader();
assertThat(((Savepoint) pf.getProxy(cl)).getSavepointName()).isEqualTo("sp");
}
@Order(2)
public static class A implements Runnable {
@@ -391,7 +428,7 @@ public class ProxyFactoryTests {
@Order(1)
public static class B implements Runnable{
public static class B implements Runnable {
@Override
public void run() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -125,7 +125,7 @@ public class FieldError extends ObjectError {
@Override
public String toString() {
return "Field error in object '" + getObjectName() + "' on field '" + this.field +
"': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " +
"': rejected value [" + ObjectUtils.nullSafeConciseToString(this.rejectedValue) + "]; " +
resolvableToString();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -133,7 +133,7 @@ public class DateFormattingTests {
assertThat(exception)
.hasMessageContaining("for property 'styleDate'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '99/01/01'")
.hasMessageContaining("for value [99/01/01]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [99/01/01]")
.hasCauseInstanceOf(ParseException.class).cause()
@@ -353,7 +353,7 @@ public class DateFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '210302'")
.hasMessageContaining("for value [210302]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(ParseException.class).cause()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -333,7 +333,7 @@ class DateTimeFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'isoLocalDate'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '2009-31-10'")
.hasMessageContaining("for value [2009-31-10]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [2009-31-10]")
.hasCauseInstanceOf(DateTimeParseException.class).cause()
@@ -540,7 +540,7 @@ class DateTimeFormattingTests {
assertThat(fieldError.unwrap(TypeMismatchException.class))
.hasMessageContaining("for property 'patternLocalDateWithFallbackPatterns'")
.hasCauseInstanceOf(ConversionFailedException.class).cause()
.hasMessageContaining("for value '210302'")
.hasMessageContaining("for value [210302]")
.hasCauseInstanceOf(IllegalArgumentException.class).cause()
.hasMessageContaining("Parse attempt failed for value [210302]")
.hasCauseInstanceOf(DateTimeParseException.class).cause()
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,7 +49,7 @@ public class ConversionFailedException extends ConversionException {
@Nullable Object value, Throwable cause) {
super("Failed to convert from type [" + sourceType + "] to type [" + targetType +
"] for value '" + ObjectUtils.nullSafeToString(value) + "'", cause);
"] for value [" + ObjectUtils.nullSafeConciseToString(value) + "]", cause);
this.sourceType = sourceType;
this.targetType = targetType;
this.value = value;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import org.apache.commons.logging.Log;
import org.springframework.lang.Nullable;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
/**
* Utility methods for formatting and logging messages.
@@ -78,7 +79,7 @@ public abstract class LogFormatUtils {
result = ObjectUtils.nullSafeToString(ex);
}
if (maxLength != -1) {
result = (result.length() > maxLength ? result.substring(0, maxLength) + " (truncated)..." : result);
result = StringUtils.truncate(result, maxLength);
}
if (replaceNewlinesAndControlCharacters) {
result = NEWLINE_PATTERN.matcher(result).replaceAll("<EOL>");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,8 +17,13 @@
package org.springframework.util;
import java.lang.reflect.Array;
import java.net.URI;
import java.net.URL;
import java.time.temporal.Temporal;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.StringJoiner;
@@ -653,6 +658,7 @@ public abstract class ObjectUtils {
* Returns a {@code "null"} String if {@code obj} is {@code null}.
* @param obj the object to build a String representation for
* @return a String representation of {@code obj}
* @see #nullSafeConciseToString(Object)
*/
public static String nullSafeToString(@Nullable Object obj) {
if (obj == null) {
@@ -908,4 +914,73 @@ public abstract class ObjectUtils {
return stringJoiner.toString();
}
/**
* Generate a null-safe, concise string representation of the supplied object
* as described below.
* <p>Favor this method over {@link #nullSafeToString(Object)} when you need
* the length of the generated string to be limited.
* <p>Returns:
* <ul>
* <li>{@code "null"} if {@code obj} is {@code null}</li>
* <li>{@linkplain Class#getName() Class name} if {@code obj} is a {@link Class}</li>
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
* if {@code obj} is a {@link String} or {@link CharSequence}</li>
* <li>Potentially {@linkplain StringUtils#truncate(CharSequence) truncated string}
* if {@code obj} is a <em>simple type</em> whose {@code toString()} method returns
* a non-null value.</li>
* <li>Otherwise, a string representation of the object's type name concatenated
* with {@code @} and a hex string form of the object's identity hash code</li>
* </ul>
* <p>In the context of this method, a <em>simple type</em> is any of the following:
* a primitive wrapper (excluding {@link Void}), an {@link Enum}, a {@link Number},
* a {@link Date}, a {@link Temporal}, a {@link URI}, a {@link URL}, or a {@link Locale}.
* @param obj the object to build a string representation for
* @return a concise string representation of the supplied object
* @since 5.3.27
* @see #nullSafeToString(Object)
* @see StringUtils#truncate(CharSequence)
*/
public static String nullSafeConciseToString(@Nullable Object obj) {
if (obj == null) {
return "null";
}
if (obj instanceof Class<?>) {
return ((Class<?>) obj).getName();
}
if (obj instanceof CharSequence) {
return StringUtils.truncate((CharSequence) obj);
}
Class<?> type = obj.getClass();
if (isSimpleValueType(type)) {
String str = obj.toString();
if (str != null) {
return StringUtils.truncate(str);
}
}
return type.getTypeName() + "@" + getIdentityHexString(obj);
}
/**
* Copy of {@link org.springframework.beans.BeanUtils#isSimpleValueType(Class)}.
* <p>Check if the given type represents a "simple" value type: a primitive or
* primitive wrapper, an enum, a String or other CharSequence, a Number, a
* Date, a Temporal, a URI, a URL, a Locale, or a Class.
* <p>{@code Void} and {@code void} are not considered simple value types.
* @param type the type to check
* @return whether the given type represents a "simple" value type
*/
private static boolean isSimpleValueType(Class<?> type) {
return (Void.class != type && void.class != type &&
(ClassUtils.isPrimitiveOrWrapper(type) ||
Enum.class.isAssignableFrom(type) ||
CharSequence.class.isAssignableFrom(type) ||
Number.class.isAssignableFrom(type) ||
Date.class.isAssignableFrom(type) ||
Temporal.class.isAssignableFrom(type) ||
URI.class == type ||
URL.class == type ||
Locale.class == type ||
Class.class == type));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -75,6 +75,10 @@ public abstract class StringUtils {
private static final char EXTENSION_SEPARATOR = '.';
private static final int DEFAULT_TRUNCATION_THRESHOLD = 100;
private static final String TRUNCATION_SUFFIX = " (truncated)...";
//---------------------------------------------------------------------
// General convenience methods for working with Strings
@@ -1388,4 +1392,40 @@ public abstract class StringUtils {
return arrayToDelimitedString(arr, ",");
}
/**
* Truncate the supplied {@link CharSequence}.
* <p>Delegates to {@link #truncate(CharSequence, int)}, supplying {@code 100}
* as the threshold.
* @param charSequence the {@code CharSequence} to truncate
* @return a truncated string, or a string representation of the original
* {@code CharSequence} if its length does not exceed the threshold
* @since 5.3.27
*/
public static String truncate(CharSequence charSequence) {
return truncate(charSequence, DEFAULT_TRUNCATION_THRESHOLD);
}
/**
* Truncate the supplied {@link CharSequence}.
* <p>If the length of the {@code CharSequence} is greater than the threshold,
* this method returns a {@linkplain CharSequence#subSequence(int, int)
* subsequence} of the {@code CharSequence} (up to the threshold) appended
* with the suffix {@code " (truncated)..."}. Otherwise, this method returns
* {@code charSequence.toString()}.
* @param charSequence the {@code CharSequence} to truncate
* @param threshold the maximum length after which to truncate; must be a
* positive number
* @return a truncated string, or a string representation of the original
* {@code CharSequence} if its length does not exceed the threshold
* @since 5.3.27
*/
public static String truncate(CharSequence charSequence, int threshold) {
Assert.isTrue(threshold > 0,
() -> "Truncation threshold must be a positive number: " + threshold);
if (charSequence.length() > threshold) {
return charSequence.subSequence(0, threshold) + TRUNCATION_SUFFIX;
}
return charSequence.toString();
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,15 +17,25 @@
package org.springframework.util;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.springframework.util.ObjectUtils.isEmpty;
@@ -816,7 +826,144 @@ class ObjectUtilsTests {
.withMessage("Constant [bogus] does not exist in enum type org.springframework.util.ObjectUtilsTests$Tropes");
}
private void assertEqualHashCodes(int expected, Object array) {
@Nested
class NullSafeConciseToStringTests {
private final String truncated = " (truncated)...";
private final int truncatedLength = 100 + truncated.length();
@Test
void nullSafeConciseToStringForNull() {
assertThat(ObjectUtils.nullSafeConciseToString(null)).isEqualTo("null");
}
@Test
void nullSafeConciseToStringForClass() {
assertThat(ObjectUtils.nullSafeConciseToString(String.class)).isEqualTo("java.lang.String");
}
@Test
void nullSafeConciseToStringForStrings() {
String repeat100 = repeat("X", 100);
String repeat101 = repeat("X", 101);
assertThat(ObjectUtils.nullSafeConciseToString("")).isEqualTo("");
assertThat(ObjectUtils.nullSafeConciseToString("foo")).isEqualTo("foo");
assertThat(ObjectUtils.nullSafeConciseToString(repeat100)).isEqualTo(repeat100);
assertThat(ObjectUtils.nullSafeConciseToString(repeat101)).hasSize(truncatedLength).endsWith(truncated);
}
@Test
void nullSafeConciseToStringForStringBuilders() {
String repeat100 = repeat("X", 100);
String repeat101 = repeat("X", 101);
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder("foo"))).isEqualTo("foo");
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat100))).isEqualTo(repeat100);
assertThat(ObjectUtils.nullSafeConciseToString(new StringBuilder(repeat101))).hasSize(truncatedLength).endsWith(truncated);
}
@Test
void nullSafeConciseToStringForEnum() {
assertThat(ObjectUtils.nullSafeConciseToString(Tropes.FOO)).isEqualTo("FOO");
}
@Test
void nullSafeConciseToStringForNumber() {
assertThat(ObjectUtils.nullSafeConciseToString(42L)).isEqualTo("42");
assertThat(ObjectUtils.nullSafeConciseToString(99.1234D)).isEqualTo("99.1234");
}
@Test
void nullSafeConciseToStringForDate() {
Date date = new Date();
assertThat(ObjectUtils.nullSafeConciseToString(date)).isEqualTo(date.toString());
}
@Test
void nullSafeConciseToStringForTemporal() {
LocalDate localDate = LocalDate.now();
assertThat(ObjectUtils.nullSafeConciseToString(localDate)).isEqualTo(localDate.toString());
}
@Test
void nullSafeConciseToStringForUri() {
String uri = "https://www.example.com/?foo=1&bar=2&baz=3";
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri))).isEqualTo(uri);
uri += "&qux=" + repeat("4", 60);
assertThat(ObjectUtils.nullSafeConciseToString(URI.create(uri)))
.hasSize(truncatedLength)
.startsWith(uri.subSequence(0, 100))
.endsWith(truncated);
}
@Test
void nullSafeConciseToStringForUrl() throws Exception {
String url = "https://www.example.com/?foo=1&bar=2&baz=3";
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url))).isEqualTo(url);
url += "&qux=" + repeat("4", 60);
assertThat(ObjectUtils.nullSafeConciseToString(new URL(url)))
.hasSize(truncatedLength)
.startsWith(url.subSequence(0, 100))
.endsWith(truncated);
}
@Test
void nullSafeConciseToStringForLocale() {
assertThat(ObjectUtils.nullSafeConciseToString(Locale.GERMANY)).isEqualTo("de_DE");
}
@Test
void nullSafeConciseToStringForArraysAndCollections() {
List<String> list = Arrays.asList("a", "b", "c");
assertThat(ObjectUtils.nullSafeConciseToString(new int[][] {{1, 2}, {3, 4}})).startsWith(prefix(int[][].class));
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray(new Object[0]))).startsWith(prefix(Object[].class));
assertThat(ObjectUtils.nullSafeConciseToString(list.toArray(new String[0]))).startsWith(prefix(String[].class));
assertThat(ObjectUtils.nullSafeConciseToString(new ArrayList<>(list))).startsWith(prefix(ArrayList.class));
assertThat(ObjectUtils.nullSafeConciseToString(new HashSet<>(list))).startsWith(prefix(HashSet.class));
}
@Test
void nullSafeConciseToStringForCustomTypes() {
class ExplosiveType {
@Override
public String toString() {
throw new UnsupportedOperationException("no-go");
}
}
ExplosiveType explosiveType = new ExplosiveType();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(explosiveType::toString);
assertThat(ObjectUtils.nullSafeConciseToString(explosiveType)).startsWith(prefix(ExplosiveType.class));
class WordyType {
@Override
public String toString() {
return repeat("blah blah", 20);
}
}
WordyType wordyType = new WordyType();
assertThat(wordyType).asString().hasSizeGreaterThanOrEqualTo(180 /* 9x20 */);
assertThat(ObjectUtils.nullSafeConciseToString(wordyType)).startsWith(prefix(WordyType.class));
}
private String repeat(String str, int count) {
String result = "";
for (int i = 0; i < count; i++) {
result += str;
}
return result;
}
private String prefix(Class<?> clazz) {
return clazz.getTypeName() + "@";
}
}
private static void assertEqualHashCodes(int expected, Object array) {
int actual = ObjectUtils.nullSafeHashCode(array);
assertThat(actual).isEqualTo(expected);
assertThat(array.hashCode() != actual).isTrue();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,6 +22,8 @@ import java.util.Locale;
import java.util.Properties;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -785,4 +787,26 @@ class StringUtilsTests {
assertThat(StringUtils.collectionToCommaDelimitedString(Collections.singletonList(null))).isEqualTo("null");
}
@Test
void truncatePreconditions() {
assertThatIllegalArgumentException()
.isThrownBy(() -> StringUtils.truncate("foo", 0))
.withMessage("Truncation threshold must be a positive number: 0");
assertThatIllegalArgumentException()
.isThrownBy(() -> StringUtils.truncate("foo", -99))
.withMessage("Truncation threshold must be a positive number: -99");
}
@ParameterizedTest
@CsvSource(delimiterString = "-->", value = {
"'' --> ''",
"aardvark --> aardvark",
"aardvark12 --> aardvark12",
"aardvark123 --> aardvark12 (truncated)...",
"aardvark, bird, cat --> aardvark, (truncated)..."
})
void truncate(String text, String truncated) {
assertThat(StringUtils.truncate(text, 10)).isEqualTo(truncated);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.expression;
import java.util.List;
import java.util.function.Supplier;
import org.springframework.lang.Nullable;
@@ -24,12 +25,21 @@ import org.springframework.lang.Nullable;
* Expressions are executed in an evaluation context. It is in this context that
* references are resolved when encountered during expression evaluation.
*
* <p>There is a default implementation of this EvaluationContext interface:
* {@link org.springframework.expression.spel.support.StandardEvaluationContext}
* which can be extended, rather than having to implement everything manually.
* <p>There are two default implementations of this interface.
* <ul>
* <li>{@link org.springframework.expression.spel.support.SimpleEvaluationContext
* SimpleEvaluationContext}: a simpler builder-style {@code EvaluationContext}
* variant for data-binding purposes, which allows for opting into several SpEL
* features as needed.</li>
* <li>{@link org.springframework.expression.spel.support.StandardEvaluationContext
* StandardEvaluationContext}: a powerful and highly configurable {@code EvaluationContext}
* implementation, which can be extended, rather than having to implement everything
* manually.</li>
* </ul>
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public interface EvaluationContext {
@@ -85,7 +95,30 @@ public interface EvaluationContext {
OperatorOverloader getOperatorOverloader();
/**
* Set a named variable within this evaluation context to a specified value.
* Assign the value created by the specified {@link Supplier} to a named variable
* within this evaluation context.
* <p>In contrast to {@link #setVariable(String, Object)}, this method should only
* be invoked to support the assignment operator ({@code =}) within an expression.
* <p>By default, this method delegates to {@code setVariable(String, Object)},
* providing the value created by the {@code valueSupplier}. Concrete implementations
* may override this <em>default</em> method to provide different semantics.
* @param name the name of the variable to assign
* @param valueSupplier the supplier of the value to be assigned to the variable
* @return a {@link TypedValue} wrapping the assigned value
* @since 5.2.24
*/
default TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
TypedValue typedValue = valueSupplier.get();
setVariable(name, typedValue.getValue());
return typedValue;
}
/**
* Set a named variable in this evaluation context to a specified value.
* <p>In contrast to {@link #assignVariable(String, Supplier)}, this method
* should only be invoked programmatically when interacting directly with the
* {@code EvaluationContext} &mdash; for example, to provide initial
* configuration for the context.
* @param name the name of the variable to set
* @param value the value to be placed in the variable
*/
@@ -93,7 +126,7 @@ public interface EvaluationContext {
/**
* Look up a named variable within this evaluation context.
* @param name variable to lookup
* @param name the name of the variable to look up
* @return the value of the variable, or {@code null} if not found
*/
@Nullable
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,6 +23,7 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.function.Supplier;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.EvaluationContext;
@@ -38,18 +39,19 @@ import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
/**
* An ExpressionState is for maintaining per-expression-evaluation state, any changes to
* it are not seen by other expressions but it gives a place to hold local variables and
* ExpressionState is for maintaining per-expression-evaluation state: any changes to
* it are not seen by other expressions, but it gives a place to hold local variables and
* for component expressions in a compound expression to communicate state. This is in
* contrast to the EvaluationContext, which is shared amongst expression evaluations, and
* any changes to it will be seen by other expressions or any code that chooses to ask
* questions of the context.
*
* <p>It also acts as a place for to define common utility routines that the various AST
* <p>It also acts as a place to define common utility routines that the various AST
* nodes might need.
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public class ExpressionState {
@@ -138,6 +140,29 @@ public class ExpressionState {
return this.scopeRootObjects.element();
}
/**
* Assign the value created by the specified {@link Supplier} to a named variable
* within the evaluation context.
* <p>In contrast to {@link #setVariable(String, Object)}, this method should
* only be invoked to support assignment within an expression.
* @param name the name of the variable to assign
* @param valueSupplier the supplier of the value to be assigned to the variable
* @return a {@link TypedValue} wrapping the assigned value
* @since 5.2.24
* @see EvaluationContext#assignVariable(String, Supplier)
*/
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
return this.relatedContext.assignVariable(name, valueSupplier);
}
/**
* Set a named variable in the evaluation context to a specified value.
* <p>In contrast to {@link #assignVariable(String, Supplier)}, this method
* should only be invoked programmatically.
* @param name the name of the variable to set
* @param value the value to be placed in the variable
* @see EvaluationContext#setVariable(String, Object)
*/
public void setVariable(String name, @Nullable Object value) {
this.relatedContext.setVariable(name, value);
}
@@ -266,13 +266,25 @@ public enum SpelMessage {
MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED(Kind.ERROR, 1075,
"Array declares too many elements, exceeding the threshold of ''{0}''"),
/** @since 5.3.26 */
/** @since 5.2.23 */
MAX_REPEATED_TEXT_SIZE_EXCEEDED(Kind.ERROR, 1076,
"Repeated text results in too many characters, exceeding the threshold of ''{0}''"),
"Repeated text is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.3.26 */
/** @since 5.2.23 */
MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077,
"Regular expression contains too many characters, exceeding the threshold of ''{0}''");
"Regular expression is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.24 */
MAX_CONCATENATED_STRING_LENGTH_EXCEEDED(Kind.ERROR, 1078,
"Concatenated string is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.24 */
MAX_EXPRESSION_LENGTH_EXCEEDED(Kind.ERROR, 1079,
"SpEL expression is too long, exceeding the threshold of ''{0}'' characters"),
/** @since 5.2.24 */
VARIABLE_ASSIGNMENT_NOT_SUPPORTED(Kind.ERROR, 1080,
"Assignment to variable ''{0}'' is not supported");
private final Kind kind;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@ import org.springframework.expression.spel.ExpressionState;
* <p>Example: 'someNumberProperty=42'
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class Assign extends SpelNodeImpl {
@@ -38,9 +39,7 @@ public class Assign extends SpelNodeImpl {
@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
TypedValue newValue = this.children[1].getValueInternal(state);
getChild(0).setValue(state, newValue.getValue());
return newValue;
return this.children[0].setValueInternal(state, () -> this.children[1].getValueInternal(state));
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
package org.springframework.expression.spel.ast;
import java.util.StringJoiner;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationException;
@@ -24,13 +25,13 @@ import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.lang.Nullable;
/**
* Represents a DOT separated expression sequence, such as
* {@code 'property1.property2.methodOne()'}.
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class CompoundExpression extends SpelNodeImpl {
@@ -95,8 +96,12 @@ public class CompoundExpression extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object value) throws EvaluationException {
getValueRef(state).setValue(value);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
getValueRef(state).setValue(typedValue.getValue());
return typedValue;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -25,6 +25,7 @@ import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.StringJoiner;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.core.convert.TypeDescriptor;
@@ -45,11 +46,12 @@ import org.springframework.util.ReflectionUtils;
/**
* An Indexer can index into some proceeding structure to access a particular piece of it.
* Supported structures are: strings / collections (lists/sets) / arrays.
* <p>Supported structures are: strings / collections (lists/sets) / arrays.
*
* @author Andy Clement
* @author Phillip Webb
* @author Stephane Nicoll
* @author Sam Brannen
* @since 3.0
*/
// TODO support multidimensional arrays
@@ -102,8 +104,12 @@ public class Indexer extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
getValueRef(state).setValue(newValue);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
getValueRef(state).setValue(typedValue.getValue());
return typedValue;
}
@Override
@@ -56,7 +56,7 @@ public class OpMultiply extends Operator {
/**
* Maximum number of characters permitted in repeated text.
* @since 5.3.26
* @since 5.2.23
*/
private static final int MAX_REPEATED_TEXT_SIZE = 256;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -27,6 +27,8 @@ import org.springframework.expression.TypeConverter;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;
@@ -46,10 +48,18 @@ import org.springframework.util.NumberUtils;
* @author Juergen Hoeller
* @author Ivo Smid
* @author Giovanni Dall'Oglio Risso
* @author Sam Brannen
* @since 3.0
*/
public class OpPlus extends Operator {
/**
* Maximum number of characters permitted in a concatenated string.
* @since 5.2.24
*/
private static final int MAX_CONCATENATED_STRING_LENGTH = 100_000;
public OpPlus(int startPos, int endPos, SpelNodeImpl... operands) {
super("+", startPos, endPos, operands);
Assert.notEmpty(operands, "Operands must not be empty");
@@ -123,22 +133,45 @@ public class OpPlus extends Operator {
if (leftOperand instanceof String && rightOperand instanceof String) {
this.exitTypeDescriptor = "Ljava/lang/String";
return new TypedValue((String) leftOperand + rightOperand);
String leftString = (String) leftOperand;
String rightString = (String) rightOperand;
checkStringLength(leftString);
checkStringLength(rightString);
return concatenate(leftString, rightString);
}
if (leftOperand instanceof String) {
return new TypedValue(
leftOperand + (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state)));
String leftString = (String) leftOperand;
checkStringLength(leftString);
String rightString = (rightOperand == null ? "null" : convertTypedValueToString(operandTwoValue, state));
checkStringLength(rightString);
return concatenate(leftString, rightString);
}
if (rightOperand instanceof String) {
return new TypedValue(
(leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state)) + rightOperand);
String rightString = (String) rightOperand;
checkStringLength(rightString);
String leftString = (leftOperand == null ? "null" : convertTypedValueToString(operandOneValue, state));
checkStringLength(leftString);
return concatenate(leftString, rightString);
}
return state.operate(Operation.ADD, leftOperand, rightOperand);
}
private void checkStringLength(String string) {
if (string.length() > MAX_CONCATENATED_STRING_LENGTH) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, MAX_CONCATENATED_STRING_LENGTH);
}
}
private TypedValue concatenate(String leftString, String rightString) {
String result = leftString + rightString;
checkStringLength(result);
return new TypedValue(result);
}
@Override
public String toStringAST() {
if (this.children.length < 2) { // unary plus
@@ -45,16 +45,16 @@ public class OperatorMatches extends Operator {
/**
* Maximum number of characters permitted in a regular expression.
* @since 5.3.26
* @since 5.2.23
*/
private static final int MAX_REGEX_LENGTH = 256;
private static final int MAX_REGEX_LENGTH = 1000;
private final ConcurrentMap<String, Pattern> patternCache;
/**
* Create a new {@link OperatorMatches} instance.
* @deprecated as of Spring Framework 5.3.26 in favor of invoking
* @deprecated as of Spring Framework 5.2.23 in favor of invoking
* {@link #OperatorMatches(ConcurrentMap, int, int, SpelNodeImpl...)}
* with a shared pattern cache instead
*/
@@ -65,7 +65,7 @@ public class OperatorMatches extends Operator {
/**
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
* @since 5.3.26
* @since 5.2.23
*/
public OperatorMatches(ConcurrentMap<String, Pattern> patternCache, int startPos, int endPos, SpelNodeImpl... operands) {
super("matches", startPos, endPos, operands);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.asm.Label;
import org.springframework.asm.MethodVisitor;
@@ -46,6 +47,7 @@ import org.springframework.util.ReflectionUtils;
* @author Andy Clement
* @author Juergen Hoeller
* @author Clark Duplichien
* @author Sam Brannen
* @since 3.0
*/
public class PropertyOrFieldReference extends SpelNodeImpl {
@@ -147,8 +149,12 @@ public class PropertyOrFieldReference extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object newValue) throws EvaluationException {
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, newValue);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
TypedValue typedValue = valueSupplier.get();
writeProperty(state.getActiveContextObject(), state.getEvaluationContext(), this.name, typedValue.getValue());
return typedValue;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.expression.spel.ast;
import java.lang.reflect.Constructor;
import java.lang.reflect.Member;
import java.lang.reflect.Method;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.asm.Opcodes;
@@ -40,6 +41,7 @@ import org.springframework.util.ObjectUtils;
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public abstract class SpelNodeImpl implements SpelNode, Opcodes {
@@ -64,7 +66,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
* <p>The descriptor is like the bytecode form but is slightly easier to work with.
* It does not include the trailing semicolon (for non array reference types).
* Some examples: Ljava/lang/String, I, [I
*/
*/
@Nullable
protected volatile String exitTypeDescriptor;
@@ -83,8 +85,8 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
/**
* Return {@code true} if the next child is one of the specified classes.
*/
* Return {@code true} if the next child is one of the specified classes.
*/
protected boolean nextChildIs(Class<?>... classes) {
if (this.parent != null) {
SpelNodeImpl[] peers = this.parent.children;
@@ -125,6 +127,28 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes {
@Override
public void setValue(ExpressionState expressionState, @Nullable Object newValue) throws EvaluationException {
setValueInternal(expressionState, () -> new TypedValue(newValue));
}
/**
* Evaluate the expression to a node and then set the new value created by the
* specified {@link Supplier} on that node.
* <p>For example, if the expression evaluates to a property reference, then the
* property will be set to the new value.
* <p>Favor this method over {@link #setValue(ExpressionState, Object)} when
* the value should be lazily computed.
* <p>By default, this method throws a {@link SpelEvaluationException},
* effectively disabling this feature. Subclasses may override this method to
* provide an actual implementation.
* @param expressionState the current expression state (includes the context)
* @param valueSupplier a supplier of the new value
* @throws EvaluationException if any problem occurs evaluating the expression or
* setting the new value
* @since 5.2.24
*/
public TypedValue setValueInternal(ExpressionState expressionState, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
throw new SpelEvaluationException(getStartPosition(), SpelMessage.SETVALUE_NOT_SUPPORTED, getClass());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,9 +17,11 @@
package org.springframework.expression.spel.ast;
import java.lang.reflect.Modifier;
import java.util.function.Supplier;
import org.springframework.asm.MethodVisitor;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.spel.CodeFlow;
import org.springframework.expression.spel.ExpressionState;
@@ -27,10 +29,11 @@ import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.lang.Nullable;
/**
* Represents a variable reference, eg. #someVar. Note this is different to a *local*
* variable like $someVar
* Represents a variable reference &mdash; for example, {@code #someVar}. Note
* that this is different than a <em>local</em> variable like {@code $someVar}.
*
* @author Andy Clement
* @author Sam Brannen
* @since 3.0
*/
public class VariableReference extends SpelNodeImpl {
@@ -53,14 +56,14 @@ public class VariableReference extends SpelNodeImpl {
@Override
public ValueRef getValueRef(ExpressionState state) throws SpelEvaluationException {
if (this.name.equals(THIS)) {
return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(),this);
return new ValueRef.TypedValueHolderValueRef(state.getActiveContextObject(), this);
}
if (this.name.equals(ROOT)) {
return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(),this);
return new ValueRef.TypedValueHolderValueRef(state.getRootContextObject(), this);
}
TypedValue result = state.lookupVariable(this.name);
// a null value will mean either the value was null or the variable was not found
return new VariableRef(this.name,result,state.getEvaluationContext());
return new VariableRef(this.name, result, state.getEvaluationContext());
}
@Override
@@ -90,8 +93,10 @@ public class VariableReference extends SpelNodeImpl {
}
@Override
public void setValue(ExpressionState state, @Nullable Object value) throws SpelEvaluationException {
state.setVariable(this.name, value);
public TypedValue setValueInternal(ExpressionState state, Supplier<TypedValue> valueSupplier)
throws EvaluationException {
return state.assignVariable(this.name, valueSupplier);
}
@Override
@@ -29,6 +29,7 @@ import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateAwareExpressionParser;
import org.springframework.expression.spel.InternalParseException;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.SpelParseException;
import org.springframework.expression.spel.SpelParserConfiguration;
@@ -92,6 +93,12 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
private static final Pattern VALID_QUALIFIED_ID_PATTERN = Pattern.compile("[\\p{L}\\p{N}_$]+");
/**
* Maximum length permitted for a SpEL expression.
* @since 5.2.24
*/
private static final int MAX_EXPRESSION_LENGTH = 10_000;
private final SpelParserConfiguration configuration;
@@ -127,6 +134,8 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException {
checkExpressionLength(expressionString);
try {
this.expressionString = expressionString;
Tokenizer tokenizer = new Tokenizer(expressionString);
@@ -148,6 +157,12 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
}
}
private void checkExpressionLength(String string) {
if (string.length() > MAX_EXPRESSION_LENGTH) {
throw new SpelEvaluationException(SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED, MAX_EXPRESSION_LENGTH);
}
}
// expression
// : logicalOrExpression
// ( (ASSIGN^ logicalOrExpression)
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
@@ -78,6 +79,7 @@ import org.springframework.lang.Nullable;
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Sam Brannen
* @since 4.3.15
* @see #forPropertyAccessors
* @see #forReadOnlyDataBinding()
@@ -200,6 +202,17 @@ public final class SimpleEvaluationContext implements EvaluationContext {
return this.operatorOverloader;
}
/**
* {@code SimpleEvaluationContext} does not support variable assignment within
* expressions.
* @throws SpelEvaluationException with {@link SpelMessage#VARIABLE_ASSIGNMENT_NOT_SUPPORTED}
* @since 5.2.24
*/
@Override
public TypedValue assignVariable(String name, Supplier<TypedValue> valueSupplier) {
throw new SpelEvaluationException(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED, "#" + name);
}
@Override
public void setVariable(String name, @Nullable Object value) {
this.variables.put(name, value);
@@ -25,6 +25,8 @@ import java.util.Map;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.expression.AccessException;
import org.springframework.expression.BeanResolver;
@@ -36,6 +38,7 @@ import org.springframework.expression.MethodFilter;
import org.springframework.expression.ParseException;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.expression.spel.testresources.TestPerson;
@@ -60,6 +63,20 @@ class EvaluationTests extends AbstractExpressionTests {
@Nested
class MiscellaneousTests {
@Test
void expressionLength() {
String expression = String.format("'X' + '%s'", repeat(" ", 9_992));
assertThat(expression).hasSize(10_000);
Expression expr = parser.parseExpression(expression);
String result = expr.getValue(context, String.class);
assertThat(result).hasSize(9_993);
assertThat(result.trim()).isEqualTo("X");
expression = String.format("'X' + '%s'", repeat(" ", 9_993));
assertThat(expression).hasSize(10_001);
evaluateAndCheckError(expression, String.class, SpelMessage.MAX_EXPRESSION_LENGTH_EXCEEDED);
}
@Test
void createListsOnAttemptToIndexNull01() throws EvaluationException, ParseException {
ExpressionParser parser = new SpelExpressionParser(new SpelParserConfiguration(true, true));
@@ -135,8 +152,24 @@ class EvaluationTests extends AbstractExpressionTests {
// assignment
@Test
void assignmentToVariables() {
evaluate("#var1='value1'", "value1", String.class);
void assignmentToVariableWithStandardEvaluationContext() {
evaluate("#var1 = 'value1'", "value1", String.class);
}
@ParameterizedTest
@CsvSource(delimiterString = "->", value = {
"'#var1 = \"value1\"' -> #var1",
"'true ? #myVar = 4 : 0' -> #myVar"
})
void assignmentToVariableWithSimpleEvaluationContext(String expression, String varName) {
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
Expression expr = parser.parseExpression(expression);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expr.getValue(context))
.satisfies(ex -> {
assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.VARIABLE_ASSIGNMENT_NOT_SUPPORTED);
assertThat(ex.getInserts()).as("inserts").containsExactly(varName);
});
}
@Test
@@ -482,15 +515,13 @@ class EvaluationTests extends AbstractExpressionTests {
@Test
void matchesWithPatternLengthThreshold() {
String pattern = "(0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +
"0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" +
"01234567890123456789012345678901234567890123456789|abc)";
assertThat(pattern).hasSize(256);
Expression expr = parser.parseExpression("'abc' matches '" + pattern + "'");
String pattern = String.format("^(%s|X)", repeat("12345", 199));
assertThat(pattern).hasSize(1000);
Expression expr = parser.parseExpression("'X' matches '" + pattern + "'");
assertThat(expr.getValue(context, Boolean.class)).isTrue();
pattern += "?";
assertThat(pattern).hasSize(257);
assertThat(pattern).hasSize(1001);
evaluateAndCheckError("'abc' matches '" + pattern + "'", Boolean.class, SpelMessage.MAX_REGEX_LENGTH_EXCEEDED);
}
@@ -1468,6 +1499,15 @@ class EvaluationTests extends AbstractExpressionTests {
}
private static String repeat(String str, int count) {
String result = "";
for (int i = 0; i < count; i++) {
result += str;
}
return result;
}
@SuppressWarnings("rawtypes")
static class TestClass {
@@ -26,6 +26,7 @@ import org.springframework.expression.spel.ast.Operator;
import org.springframework.expression.spel.standard.SpelExpression;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.expression.spel.SpelMessage.MAX_CONCATENATED_STRING_LENGTH_EXCEEDED;
import static org.springframework.expression.spel.SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED;
/**
@@ -391,11 +392,7 @@ class OperatorTests extends AbstractExpressionTests {
evaluate("3.0f + 5.0f", 8.0f, Float.class);
evaluate("3.0d + 5.0d", 8.0d, Double.class);
evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class);
evaluate("'ab' + 2", "ab2", String.class);
evaluate("2 + 'a'", "2a", String.class);
evaluate("'ab' + null", "abnull", String.class);
evaluate("null + 'ab'", "nullab", String.class);
evaluate("5 + new Integer('37')", 42, Integer.class);
// AST:
SpelExpression expr = (SpelExpression) parser.parseExpression("+3");
@@ -409,11 +406,6 @@ class OperatorTests extends AbstractExpressionTests {
evaluate("+5", 5, Integer.class);
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class);
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
// string concatenation
evaluate("'abc'+'def'", "abcdef", String.class);
evaluate("5 + new Integer('37')", 42, Integer.class);
}
@Test
@@ -587,6 +579,59 @@ class OperatorTests extends AbstractExpressionTests {
evaluateAndCheckError("'a' * 257", String.class, MAX_REPEATED_TEXT_SIZE_EXCEEDED, 4);
}
@Test
void stringConcatenation() {
evaluate("'' + ''", "", String.class);
evaluate("'' + null", "null", String.class);
evaluate("null + ''", "null", String.class);
evaluate("'ab' + null", "abnull", String.class);
evaluate("null + 'ab'", "nullab", String.class);
evaluate("'ab' + 2", "ab2", String.class);
evaluate("2 + 'ab'", "2ab", String.class);
evaluate("'abc' + 'def'", "abcdef", String.class);
// Text is big but not too big
final int maxSize = 100_000;
context.setVariable("text1", createString(maxSize));
Expression expr = parser.parseExpression("#text1 + ''");
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
expr = parser.parseExpression("'' + #text1");
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
context.setVariable("text1", createString(maxSize / 2));
expr = parser.parseExpression("#text1 + #text1");
assertThat(expr.getValue(context, String.class)).hasSize(maxSize);
// Text is too big
context.setVariable("text1", createString(maxSize + 1));
evaluateAndCheckError("#text1 + ''", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("'' + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 3);
evaluateAndCheckError("true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 5);
context.setVariable("text1", createString(maxSize / 2));
context.setVariable("text2", createString((maxSize / 2) + 1));
evaluateAndCheckError("#text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + #text2 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text1 + true + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
evaluateAndCheckError("true + #text1 + #text2", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
evaluateAndCheckError("#text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text2 + #text1 + true", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
evaluateAndCheckError("#text2 + true + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
evaluateAndCheckError("true + #text2 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 14);
context.setVariable("text1", createString((maxSize / 3) + 1));
evaluateAndCheckError("#text1 + #text1 + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 16);
evaluateAndCheckError("(#text1 + #text1) + #text1", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 18);
evaluateAndCheckError("#text1 + (#text1 + #text1)", String.class, MAX_CONCATENATED_STRING_LENGTH_EXCEEDED, 7);
}
private static String createString(int size) {
return new String(new char[size]);
}
@Test
void longs() {
evaluate("3L == 4L", false, Boolean.class);
@@ -436,7 +436,7 @@ public abstract class SharedEntityManagerCreator {
entry.setValue(storedProc.getOutputParameterValue(key.toString()));
}
}
catch (IllegalArgumentException ex) {
catch (RuntimeException ex) {
entry.setValue(ex);
}
}
@@ -125,11 +125,11 @@ public abstract class MockRestRequestMatchers {
* @param name the name of the query parameter whose value(s) will be asserted
* @param matcher the Hamcrest matcher to apply to the entire list of values
* for the given query parameter
* @since 5.3.26
* @since 5.3.27
* @see #queryParam(String, Matcher...)
* @see #queryParam(String, String...)
*/
public static RequestMatcher queryParam(String name, Matcher<? super List<String>> matcher) {
public static RequestMatcher queryParamList(String name, Matcher<? super List<String>> matcher) {
return request -> {
MultiValueMap<String, String> params = getQueryParams(request);
List<String> paramValues = params.get(name);
@@ -147,14 +147,14 @@ public abstract class MockRestRequestMatchers {
* values, effectively ignoring the additional parameter values. If the number
* of provided {@code matchers} exceeds the number of query parameter values,
* an {@link AssertionError} will be thrown to signal the mismatch.
* <p>See {@link #queryParam(String, Matcher)} for a variant which accepts a
* <p>See {@link #queryParamList(String, Matcher)} for a variant which accepts a
* {@code Matcher} that applies to the entire list of values as opposed to
* applying only to individual values.
* @param name the name of the query parameter whose value(s) will be asserted
* @param matchers the Hamcrest matchers to apply to individual query parameter
* values; the n<sup>th</sup> matcher is applied to the n<sup>th</sup> query
* parameter value
* @see #queryParam(String, Matcher)
* @see #queryParamList(String, Matcher)
* @see #queryParam(String, String...)
*/
@SafeVarargs
@@ -175,14 +175,14 @@ public abstract class MockRestRequestMatchers {
* parameter values, effectively ignoring the additional parameter values. If
* the number of {@code expectedValues} exceeds the number of query parameter
* values, an {@link AssertionError} will be thrown to signal the mismatch.
* <p>See {@link #queryParam(String, Matcher)} for a variant which accepts a
* <p>See {@link #queryParamList(String, Matcher)} for a variant which accepts a
* Hamcrest {@code Matcher} that applies to the entire list of values as opposed
* to asserting only individual values.
* @param name the name of the query parameter whose value(s) will be asserted
* @param expectedValues the expected values of individual query parameter values;
* the n<sup>th</sup> expected value is compared to the n<sup>th</sup> query
* parameter value
* @see #queryParam(String, Matcher)
* @see #queryParamList(String, Matcher)
* @see #queryParam(String, Matcher...)
*/
public static RequestMatcher queryParam(String name, String... expectedValues) {
@@ -211,11 +211,11 @@ public abstract class MockRestRequestMatchers {
* @param name the name of the header whose value(s) will be asserted
* @param matcher the Hamcrest matcher to apply to the entire list of values
* for the given header
* @since 5.3.26
* @since 5.3.27
* @see #header(String, Matcher...)
* @see #header(String, String...)
*/
public static RequestMatcher header(String name, Matcher<? super List<String>> matcher) {
public static RequestMatcher headerList(String name, Matcher<? super List<String>> matcher) {
return request -> {
List<String> headerValues = request.getHeaders().get(name);
if (headerValues == null) {
@@ -232,13 +232,13 @@ public abstract class MockRestRequestMatchers {
* effectively ignoring the additional header values. If the number of
* provided {@code matchers} exceeds the number of header values, an
* {@link AssertionError} will be thrown to signal the mismatch.
* <p>See {@link #header(String, Matcher)} for a variant which accepts a
* <p>See {@link #headerList(String, Matcher)} for a variant which accepts a
* Hamcrest {@code Matcher} that applies to the entire list of values as
* opposed to applying only to individual values.
* @param name the name of the header whose value(s) will be asserted
* @param matchers the Hamcrest matchers to apply to individual header values;
* the n<sup>th</sup> matcher is applied to the n<sup>th</sup> header value
* @see #header(String, Matcher)
* @see #headerList(String, Matcher)
* @see #header(String, String...)
*/
@SafeVarargs
@@ -260,13 +260,13 @@ public abstract class MockRestRequestMatchers {
* additional header values. If the number of {@code expectedValues} exceeds the
* number of header values, an {@link AssertionError} will be thrown to signal the
* mismatch.
* <p>See {@link #header(String, Matcher)} for a variant which accepts a
* <p>See {@link #headerList(String, Matcher)} for a variant which accepts a
* Hamcrest {@code Matcher} that applies to the entire list of values as
* opposed to applying only to individual values.
* @param name the name of the header whose value(s) will be asserted
* @param expectedValues the expected values of individual header values; the
* n<sup>th</sup> expected value is compared to the n<sup>th</sup> header value
* @see #header(String, Matcher)
* @see #headerList(String, Matcher)
* @see #header(String, Matcher...)
*/
public static RequestMatcher header(String name, String... expectedValues) {
@@ -29,19 +29,17 @@ import org.springframework.mock.http.client.MockClientHttpRequest;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.hamcrest.Matchers.allOf;
import static org.hamcrest.Matchers.any;
import static org.hamcrest.Matchers.anything;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.either;
import static org.hamcrest.Matchers.endsWith;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.hamcrest.Matchers.startsWith;
/**
@@ -164,7 +162,7 @@ class MockRestRequestMatchersTests {
@Test
void headerListMissing() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", hasSize(2)).match(this.request))
.withMessage("Expected header <foo> to exist but was null");
}
@@ -172,29 +170,18 @@ class MockRestRequestMatchersTests {
void headerListMatchers() throws IOException {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.header("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.header("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.header("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.header("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.header("foo", hasSize(2)).match(this.request);
MockRestRequestMatchers.headerList("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.headerList("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.headerList("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.headerList("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.headerList("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.headerList("foo", hasSize(2)).match(this.request);
// These can be a bit ambiguous when reading the test (the compiler selects the list matcher):
MockRestRequestMatchers.header("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.header("foo", is(anything())).match(this.request);
MockRestRequestMatchers.header("foo", allOf(notNullValue(), notNullValue())).match(this.request);
MockRestRequestMatchers.headerList("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.headerList("foo", is(anything())).match(this.request);
MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), notNullValue())).match(this.request);
// These are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
// string-oriented, or obviously a vararg of matchers
// list matcher version
MockRestRequestMatchers.header("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
// vararg version
MockRestRequestMatchers.header("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
MockRestRequestMatchers.header("foo", is((any(String.class)))).match(this.request);
MockRestRequestMatchers.header("foo", either(is("bar")).or(is(nullValue()))).match(this.request);
MockRestRequestMatchers.header("foo", is(notNullValue()), is(notNullValue())).match(this.request);
MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
}
@Test
@@ -202,27 +189,41 @@ class MockRestRequestMatchersTests {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", contains(containsString("ba"))).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", contains(containsString("ba"))).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: iterable containing [a string containing \"ba\"]",
"but: not matched: \"baz\"");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", hasItem(endsWith("ba"))).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", hasItem(endsWith("ba"))).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: a collection containing a string ending with \"ba\"",
"but: mismatches were: [was \"bar\", was \"baz\"]");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.header("foo", everyItem(endsWith("ar"))).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", everyItem(endsWith("ar"))).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: every item is a string ending with \"ar\"",
"but: an item was \"baz\"");
}
@Test
void headerListDoesntHideHeaderWithSingleMatcher() throws IOException {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
MockRestRequestMatchers.header("foo", equalTo("bar")).match(this.request);
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", equalTo("bar")).match(this.request))
.withMessageContainingAll(
"Request header [foo] values",
"Expected: \"bar\"",
"but: was <[bar, baz]>");
}
@Test
void headers() throws Exception {
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
@@ -290,7 +291,7 @@ class MockRestRequestMatchersTests {
@Test
void queryParamListMissing() {
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", hasSize(2)).match(this.request))
.withMessage("Expected query param <foo> to exist but was null");
}
@@ -298,29 +299,18 @@ class MockRestRequestMatchersTests {
void queryParamListMatchers() throws IOException {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
MockRestRequestMatchers.queryParam("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", hasSize(2)).match(this.request);
MockRestRequestMatchers.queryParamList("foo", containsInAnyOrder(endsWith("baz"), endsWith("bar"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", contains(is("bar"), is("baz"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", contains(is("bar"), anything())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", hasItem(endsWith("baz"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", everyItem(startsWith("ba"))).match(this.request);
MockRestRequestMatchers.queryParamList("foo", hasSize(2)).match(this.request);
// These can be a bit ambiguous when reading the test (the compiler selects the list matcher):
MockRestRequestMatchers.queryParam("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.queryParam("foo", is(anything())).match(this.request);
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), notNullValue())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", notNullValue()).match(this.request);
MockRestRequestMatchers.queryParamList("foo", is(anything())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), notNullValue())).match(this.request);
// These are not as ambiguous thanks to an inner matcher that is either obviously list-oriented,
// string-oriented, or obviously a vararg of matchers
// list matcher version
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
// vararg version
MockRestRequestMatchers.queryParam("foo", allOf(notNullValue(), endsWith("ar"))).match(this.request);
MockRestRequestMatchers.queryParam("foo", is((any(String.class)))).match(this.request);
MockRestRequestMatchers.queryParam("foo", either(is("bar")).or(is(nullValue()))).match(this.request);
MockRestRequestMatchers.queryParam("foo", is(notNullValue()), is(notNullValue())).match(this.request);
MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
}
@Test
@@ -328,27 +318,41 @@ class MockRestRequestMatchersTests {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", contains(containsString("ba"))).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", contains(containsString("ba"))).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: iterable containing [a string containing \"ba\"]",
"but: not matched: \"baz\"");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", hasItem(endsWith("ba"))).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", hasItem(endsWith("ba"))).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: a collection containing a string ending with \"ba\"",
"but: mismatches were: [was \"bar\", was \"baz\"]");
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", everyItem(endsWith("ar"))).match(this.request))
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", everyItem(endsWith("ar"))).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: every item is a string ending with \"ar\"",
"but: an item was \"baz\"");
}
@Test
void queryParamListDoesntHideQueryParamWithSingleMatcher() throws IOException {
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
MockRestRequestMatchers.queryParam("foo", equalTo("bar")).match(this.request);
assertThatAssertionError()
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", equalTo("bar")).match(this.request))
.withMessageContainingAll(
"Query param [foo] values",
"Expected: \"bar\"",
"but: was <[bar, baz]>");
}
private static ThrowableTypeAssert<AssertionError> assertThatAssertionError() {
return assertThatExceptionOfType(AssertionError.class);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -106,14 +106,12 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C
Function<? super ClientHttpRequest, Mono<Void>> requestCallback) {
HttpClientContext context = this.contextProvider.apply(method, uri);
if (context.getCookieStore() == null) {
context.setCookieStore(new BasicCookieStore());
}
HttpComponentsClientHttpRequest request = new HttpComponentsClientHttpRequest(method, uri,
context, this.dataBufferFactory);
HttpComponentsClientHttpRequest request =
new HttpComponentsClientHttpRequest(method, uri, context, this.dataBufferFactory);
return requestCallback.apply(request).then(Mono.defer(() -> execute(request, context)));
}
@@ -123,7 +121,6 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C
return Mono.create(sink -> {
ReactiveResponseConsumer reactiveResponseConsumer =
new ReactiveResponseConsumer(new MonoFutureCallbackAdapter(sink, this.dataBufferFactory, context));
this.client.execute(requestProducer, reactiveResponseConsumer, context, null);
});
}
@@ -133,6 +130,7 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C
this.client.close();
}
private static class MonoFutureCallbackAdapter
implements FutureCallback<Message<HttpResponse, Publisher<ByteBuffer>>> {
@@ -144,6 +142,7 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C
public MonoFutureCallbackAdapter(MonoSink<ClientHttpResponse> sink,
DataBufferFactory dataBufferFactory, HttpClientContext context) {
this.sink = sink;
this.dataBufferFactory = dataBufferFactory;
this.context = context;
@@ -151,19 +150,12 @@ public class HttpComponentsClientHttpConnector implements ClientHttpConnector, C
@Override
public void completed(Message<HttpResponse, Publisher<ByteBuffer>> result) {
HttpComponentsClientHttpResponse response =
new HttpComponentsClientHttpResponse(this.dataBufferFactory, result, this.context);
this.sink.success(response);
this.sink.success(new HttpComponentsClientHttpResponse(this.dataBufferFactory, result, this.context));
}
@Override
public void failed(Exception ex) {
Throwable t = ex;
if (t instanceof HttpStreamResetException) {
HttpStreamResetException httpStreamResetException = (HttpStreamResetException) ex;
t = httpStreamResetException.getCause();
}
this.sink.error(t);
this.sink.error(ex instanceof HttpStreamResetException && ex.getCause() != null ? ex.getCause() : ex);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -53,6 +53,7 @@ class HttpComponentsHeadersAdapter implements MultiValueMap<String, String> {
@Override
@Nullable
public String getFirst(String key) {
Header header = this.message.getFirstHeader(key);
return (header != null ? header.getValue() : null);
@@ -79,10 +79,12 @@ class ReactorServerHttpRequest extends AbstractServerHttpRequest {
return new URI(resolveBaseUrl(request) + resolveRequestUri(request));
}
private static String resolveBaseUrl(HttpServerRequest request) {
private static URI resolveBaseUrl(HttpServerRequest request) throws URISyntaxException {
String scheme = request.scheme();
int port = request.hostPort();
return scheme + "://" + request.hostName() + (usePort(scheme, port) ? ":" + port : "");
return (usePort(scheme, port) ?
new URI(scheme, null, request.hostName(), port, null, null, null) :
new URI(scheme, request.hostName(), null, null));
}
private static boolean usePort(String scheme, int port) {
+2
View File
@@ -1,6 +1,7 @@
description = "Spring WebFlux"
apply plugin: "kotlin"
apply plugin: "kotlinx-serialization"
dependencies {
api(project(":spring-beans"))
@@ -45,6 +46,7 @@ dependencies {
testImplementation('org.apache.httpcomponents.core5:httpcore5-reactive')
testImplementation("com.squareup.okhttp3:mockwebserver")
testImplementation("org.jetbrains.kotlin:kotlin-script-runtime")
testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json")
testRuntimeOnly("org.jetbrains.kotlin:kotlin-scripting-jsr223")
testRuntimeOnly("org.jruby:jruby")
testRuntimeOnly("org.python:jython-standalone")
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -139,8 +139,15 @@ public abstract class AbstractMessageWriterResultHandler extends HandlerResultHa
}
else {
publisher = Mono.justOrEmpty(body);
actualElementType = body != null ? ResolvableType.forInstance(body) : bodyType;
elementType = (bodyType.toClass() == Object.class && body != null ? actualElementType : bodyType);
ResolvableType bodyInstanceType = ResolvableType.forInstance(body);
if (bodyType.toClass() == Object.class && body != null) {
actualElementType = bodyInstanceType;
elementType = bodyInstanceType;
}
else {
actualElementType = (body == null || bodyInstanceType.hasUnresolvableGenerics()) ? bodyType : bodyInstanceType;
elementType = bodyType;
}
}
if (elementType.resolve() == void.class || elementType.resolve() == Void.class) {
@@ -112,7 +112,7 @@ public class DelegatingWebFluxConfigurationTests {
boolean condition = initializer.getValidator() instanceof LocalValidatorFactoryBean;
assertThat(condition).isTrue();
assertThat(initializer.getConversionService()).isSameAs(formatterRegistry.getValue());
assertThat(codecsConfigurer.getValue().getReaders().size()).isEqualTo(14);
assertThat(codecsConfigurer.getValue().getReaders()).hasSize(15);
}
@Test
@@ -152,7 +152,7 @@ public class WebFluxConfigurationSupportTests {
assertThat(adapter).isNotNull();
List<HttpMessageReader<?>> readers = adapter.getMessageReaders();
assertThat(readers.size()).isEqualTo(14);
assertThat(readers).hasSize(15);
ResolvableType multiValueMapType = forClassWithGenerics(MultiValueMap.class, String.class, String.class);
@@ -207,7 +207,7 @@ public class WebFluxConfigurationSupportTests {
assertThat(handler.getOrder()).isEqualTo(0);
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
assertThat(writers.size()).isEqualTo(13);
assertThat(writers).hasSize(14);
assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);
@@ -235,7 +235,7 @@ public class WebFluxConfigurationSupportTests {
assertThat(handler.getOrder()).isEqualTo(100);
List<HttpMessageWriter<?>> writers = handler.getMessageWriters();
assertThat(writers.size()).isEqualTo(13);
assertThat(writers).hasSize(14);
assertHasMessageWriter(writers, forClass(byte[].class), APPLICATION_OCTET_STREAM);
assertHasMessageWriter(writers, forClass(ByteBuffer.class), APPLICATION_OCTET_STREAM);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -62,6 +62,7 @@ import static org.springframework.web.testfixture.method.ResolvableMethod.on;
* Unit tests for {@link AbstractMessageWriterResultHandler}.
*
* @author Rossen Stoyanchev
* @author Sebastien Deleuze
*/
public class MessageWriterResultHandlerTests {
@@ -180,6 +181,17 @@ public class MessageWriterResultHandlerTests {
assertResponseBody("[{\"id\":123,\"name\":\"foo\"},{\"id\":456,\"name\":\"bar\"}]");
}
@Test
public void jacksonTypeWithSubTypeAndObjectReturnValue() {
MethodParameter returnType = on(TestController.class).resolveReturnType(Object.class);
SimpleBean body = new SimpleBean(123L, "foo");
this.resultHandler.writeBody(body, returnType, this.exchange).block(Duration.ofSeconds(5));
assertThat(this.exchange.getResponse().getHeaders().getContentType()).isEqualTo(APPLICATION_JSON);
assertResponseBody("{\"id\":123,\"name\":\"foo\"}");
}
private void assertResponseBody(String responseBody) {
StepVerifier.create(this.exchange.getResponse().getBody())
@@ -287,6 +299,8 @@ public class MessageWriterResultHandlerTests {
Identifiable identifiable() { return null; }
List<Identifiable> listIdentifiable() { return null; }
Object object() { return null; }
}
}
@@ -47,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Rossen Stoyanchev
* @author Stephane Maldini
* @author Sebastien Deleuze
* @since 5.0
*/
class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTests {
@@ -91,8 +92,8 @@ class RequestMappingIntegrationTests extends AbstractRequestMappingIntegrationTe
void stream(HttpServer httpServer) throws Exception {
startServer(httpServer);
String[] expected = {"0", "1", "2", "3", "4"};
assertThat(performGet("/stream", new HttpHeaders(), String[].class).getBody()).isEqualTo(expected);
Integer[] expected = {0, 1, 2, 3, 4};
assertThat(performGet("/stream", new HttpHeaders(), Integer[].class).getBody()).isEqualTo(expected);
}
@@ -0,0 +1,161 @@
/*
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.result.method.annotation
import kotlinx.serialization.Serializable
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.core.ResolvableType
import org.springframework.core.io.buffer.DataBuffer
import org.springframework.http.MediaType
import org.springframework.http.ResponseEntity
import org.springframework.http.codec.EncoderHttpMessageWriter
import org.springframework.http.codec.HttpMessageWriter
import org.springframework.http.codec.json.Jackson2JsonEncoder
import org.springframework.http.codec.json.KotlinSerializationJsonEncoder
import org.springframework.util.ObjectUtils
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.reactive.accept.RequestedContentTypeResolverBuilder
import org.springframework.web.testfixture.http.server.reactive.MockServerHttpRequest
import org.springframework.web.testfixture.method.ResolvableMethod
import org.springframework.web.testfixture.server.MockServerWebExchange
import reactor.test.StepVerifier
import java.nio.charset.StandardCharsets
import java.time.Duration
import java.util.*
/**
* Kotlin unit tests for {@link AbstractMessageWriterResultHandler}.
*
* @author Sebastien Deleuze
*/
class KotlinMessageWriterResultHandlerTests {
private val resultHandler = initResultHandler()
private val exchange = MockServerWebExchange.from(MockServerHttpRequest.get("/path"))
private fun initResultHandler(vararg writers: HttpMessageWriter<*>): AbstractMessageWriterResultHandler {
val writerList = if (ObjectUtils.isEmpty(writers)) {
listOf(
EncoderHttpMessageWriter(KotlinSerializationJsonEncoder()),
EncoderHttpMessageWriter(Jackson2JsonEncoder())
)
} else {
listOf(*writers)
}
val resolver = RequestedContentTypeResolverBuilder().build()
return object : AbstractMessageWriterResultHandler(writerList, resolver) {}
}
@Test
fun nonSuspendWithoutResponseEntity() {
val returnType = ResolvableMethod.on(SampleController::class.java)
.resolveReturnType(List::class.java, Person::class.java)
val body = listOf(Person(UserId(1), "John"))
resultHandler.writeBody(body, returnType, exchange).block(Duration.ofSeconds(5))
Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON)
assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]")
}
@Test
fun nonSuspendWithResponseEntity() {
val returnType = ResolvableMethod.on(SampleController::class.java)
.returning(ResolvableType.forClassWithGenerics(ResponseEntity::class.java,
ResolvableType.forClassWithGenerics(List::class.java, Person::class.java)))
.build().returnType()
val body = ResponseEntity.ok(listOf(Person(UserId(1), "John")))
resultHandler.writeBody(body.body, returnType.nested(), returnType, exchange).block(Duration.ofSeconds(5))
Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON)
assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]")
}
@Test
fun suspendWithoutResponseEntity() {
val returnType = ResolvableMethod.on(CoroutinesSampleController::class.java)
.resolveReturnType(List::class.java, Person::class.java)
val body = listOf(Person(UserId(1), "John"))
resultHandler.writeBody(body, returnType, exchange).block(Duration.ofSeconds(5))
Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON)
assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]")
}
@Test
fun suspendWithResponseEntity() {
val returnType = ResolvableMethod.on(CoroutinesSampleController::class.java)
.returning(ResolvableType.forClassWithGenerics(ResponseEntity::class.java,
ResolvableType.forClassWithGenerics(List::class.java, Person::class.java)))
.build().returnType()
val body = ResponseEntity.ok(listOf(Person(UserId(1), "John")))
resultHandler.writeBody(body.body, returnType.nested(), returnType, exchange).block(Duration.ofSeconds(5))
Assertions.assertThat(exchange.response.headers.contentType).isEqualTo(MediaType.APPLICATION_JSON)
assertResponseBody("[{\"userId\":1,\"name\":\"John\"}]")
}
private fun assertResponseBody(responseBody: String) {
StepVerifier.create(exchange.response.body)
.consumeNextWith { buf: DataBuffer ->
Assertions.assertThat(
buf.toString(StandardCharsets.UTF_8)
).isEqualTo(responseBody)
}
.expectComplete()
.verify()
}
@RestController
class SampleController {
@GetMapping("/non-suspend-with-response-entity")
fun withResponseEntity(): ResponseEntity<List<Person>> =
TODO()
@GetMapping("/non-suspend-without-response-entity")
fun withoutResponseEntity(): List<Person> =
TODO()
}
@RestController
class CoroutinesSampleController {
@GetMapping("/suspend-with-response-entity")
suspend fun suspendAndResponseEntity(): ResponseEntity<List<Person>> =
TODO()
@GetMapping("/suspend-without-response-entity")
suspend fun suspendWithoutResponseEntity(): List<Person> =
TODO()
}
@Serializable
data class Person(
val userId: UserId,
val name: String,
)
@JvmInline
@Serializable
value class UserId(val id: Int)
}
@@ -48,6 +48,7 @@ import org.springframework.web.servlet.ModelAndView;
* <a href="https://www.w3.org/TR/eventsource/">Server-Sent Events</a>.
*
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @since 5.3.2
*/
final class SseServerResponse extends AbstractServerResponse {
@@ -90,7 +91,7 @@ final class SseServerResponse extends AbstractServerResponse {
}
DefaultAsyncServerResponse.writeAsync(request, response, result);
this.sseConsumer.accept(new DefaultSseBuilder(response, context, result));
this.sseConsumer.accept(new DefaultSseBuilder(response, context, result, this.headers()));
return null;
}
@@ -113,15 +114,19 @@ final class SseServerResponse extends AbstractServerResponse {
private final List<HttpMessageConverter<?>> messageConverters;
private final HttpHeaders httpHeaders;
private final StringBuilder builder = new StringBuilder();
private boolean sendFailed;
public DefaultSseBuilder(HttpServletResponse response, Context context, DeferredResult<?> deferredResult) {
public DefaultSseBuilder(HttpServletResponse response, Context context, DeferredResult<?> deferredResult,
HttpHeaders httpHeaders) {
this.outputMessage = new ServletServerHttpResponse(response);
this.deferredResult = deferredResult;
this.messageConverters = context.messageConverters();
this.httpHeaders = httpHeaders;
}
@Override
@@ -206,7 +211,7 @@ final class SseServerResponse extends AbstractServerResponse {
for (HttpMessageConverter<?> converter : this.messageConverters) {
if (converter.canWrite(dataClass, MediaType.APPLICATION_JSON)) {
HttpMessageConverter<Object> objectConverter = (HttpMessageConverter<Object>) converter;
ServerHttpResponse response = new MutableHeadersServerHttpResponse(this.outputMessage);
ServerHttpResponse response = new MutableHeadersServerHttpResponse(this.outputMessage, this.httpHeaders);
objectConverter.write(data, MediaType.APPLICATION_JSON, response);
this.outputMessage.getBody().write(NL_NL);
this.outputMessage.flush();
@@ -276,9 +281,10 @@ final class SseServerResponse extends AbstractServerResponse {
private final HttpHeaders mutableHeaders = new HttpHeaders();
public MutableHeadersServerHttpResponse(ServerHttpResponse delegate) {
public MutableHeadersServerHttpResponse(ServerHttpResponse delegate, HttpHeaders headers) {
super(delegate);
this.mutableHeaders.putAll(delegate.getHeaders());
this.mutableHeaders.putAll(headers);
}
@Override
@@ -776,9 +776,14 @@ public class MvcUriComponentsBuilder {
}
else if (controllerType.isInterface()) {
return (T) Proxy.newProxyInstance(controllerType.getClassLoader(),
new Class<?>[] {controllerType, MethodInvocationInfo.class},
interceptor);
ClassLoader classLoader = controllerType.getClassLoader();
if (classLoader == null || classLoader.getParent() == null) {
// JDK interface type from bootstrap loader or platform loader ->
// use higher-level loader which can see Spring infrastructure classes
classLoader = MethodInvocationInfo.class.getClassLoader();
}
Class<?>[] ifcs = new Class<?>[] {controllerType, MethodInvocationInfo.class};
return (T) Proxy.newProxyInstance(classLoader, ifcs, interceptor);
}
else {
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Arjen Poutsma
* @author Sebastien Deleuze
*/
class SseServerResponseTests {
@@ -89,6 +90,33 @@ class SseServerResponseTests {
assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected);
}
@Test
void sendObjectWithPrettyPrint() throws Exception {
Person person = new Person("John Doe", 42);
ServerResponse response = ServerResponse.sse(sse -> {
try {
sse.send(person);
}
catch (IOException ex) {
throw new UncheckedIOException(ex);
}
});
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
converter.setPrettyPrint(true);
ServerResponse.Context context = () -> Collections.singletonList(converter);
ModelAndView mav = response.writeTo(this.mockRequest, this.mockResponse, context);
assertThat(mav).isNull();
String expected = "data:{\n" +
"data: \"name\" : \"John Doe\",\n" +
"data: \"age\" : 42\n" +
"data:}\n" +
"\n";
assertThat(this.mockResponse.getContentAsString()).isEqualTo(expected);
}
@Test
void builder() throws Exception {
ServerResponse response = ServerResponse.sse(sse -> {
@@ -1,5 +1,5 @@
/*
* Copyright 2012-2020 the original author or authors.
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,6 +21,7 @@ import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.sql.Savepoint;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@@ -378,7 +379,7 @@ public class MvcUriComponentsBuilderTests {
assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21");
}
@Test // SPR-16710
@Test // SPR-16710
public void fromMethodCallWithStringReturnType() {
assertThatIllegalStateException().isThrownBy(() -> {
UriComponents uriComponents = fromMethodCall(
@@ -395,9 +396,24 @@ public class MvcUriComponentsBuilderTests {
assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21");
}
@Test // gh-30210
public void fromMethodCallWithCharSequenceReturnType() {
UriComponents uriComponents = fromMethodCall(
on(BookingControllerWithCharSequence.class).getBooking(21L)).buildAndExpand(42);
assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21");
}
@Test // gh-30210
public void fromMethodCallWithJdbc30115ReturnType() {
UriComponents uriComponents = fromMethodCall(
on(BookingControllerWithJdbcSavepoint.class).getBooking(21L)).buildAndExpand(42);
assertThat(uriComponents.encode().toUri().toString()).isEqualTo("http://localhost/hotels/42/bookings/21");
}
@Test
public void fromMappingNamePlain() {
initWebApplicationContext(WebConfig.class);
this.request.setServerName("example.org");
@@ -411,7 +427,6 @@ public class MvcUriComponentsBuilderTests {
@Test
public void fromMappingNameWithCustomBaseUrl() {
initWebApplicationContext(WebConfig.class);
UriComponentsBuilder baseUrl = UriComponentsBuilder.fromUriString("https://example.org:9999/base");
@@ -420,9 +435,8 @@ public class MvcUriComponentsBuilderTests {
assertThat(url).isEqualTo("https://example.org:9999/base/people/123/addresses/DE");
}
@Test // SPR-17027
@Test // SPR-17027
public void fromMappingNameWithEncoding() {
initWebApplicationContext(WebConfig.class);
this.request.setServerName("example.org");
@@ -436,7 +450,6 @@ public class MvcUriComponentsBuilderTests {
@Test
public void fromMappingNameWithPathWithoutLeadingSlash() {
initWebApplicationContext(PathWithoutLeadingSlashConfig.class);
this.request.setServerName("example.org");
@@ -450,7 +463,6 @@ public class MvcUriComponentsBuilderTests {
@Test
public void fromControllerWithPrefix() {
initWebApplicationContext(PathPrefixWebConfig.class);
this.request.setScheme("https");
@@ -463,7 +475,6 @@ public class MvcUriComponentsBuilderTests {
@Test
public void fromMethodWithPrefix() {
initWebApplicationContext(PathPrefixWebConfig.class);
this.request.setScheme("https");
@@ -701,4 +712,26 @@ public class MvcUriComponentsBuilderTests {
}
}
@Controller
@RequestMapping("/hotels/{hotel}")
static class BookingControllerWithCharSequence {
@GetMapping("/bookings/{booking}")
public CharSequence getBooking(@PathVariable Long booking) {
return "url";
}
}
@Controller
@RequestMapping("/hotels/{hotel}")
static class BookingControllerWithJdbcSavepoint {
@GetMapping("/bookings/{booking}")
public Savepoint getBooking(@PathVariable Long booking) {
return null;
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,7 +49,7 @@ import org.springframework.context.annotation.Import;
* registry.addHandler(echoWebSocketHandler(), "/echo").withSockJS();
* }
*
* &#064;Override
* &#064;Bean
* public WebSocketHandler echoWebSocketHandler() {
* return new EchoWebSocketHandler();
* }
+1 -1
View File
@@ -12,7 +12,7 @@ Spring MVC Test, WebTestClient.
JDBC, R2DBC, O/R Mapping, XML Marshalling.
<<web.adoc#spring-web, Web Servlet>> :: Spring MVC, WebSocket, SockJS,
STOMP Messaging.
<<web-reactive.adoc#spring-webflux, Web Reactive>> :: Spring WebFlux, WebClient,
<<web-reactive.adoc#spring-web-reactive, Web Reactive>> :: Spring WebFlux, WebClient,
WebSocket, RSocket.
<<integration.adoc#spring-integration, Integration>> :: Remoting, JMS, JCA, JMX,
Email, Tasks, Scheduling, Caching.
+3 -3
View File
@@ -1625,7 +1625,7 @@ content types that a controller method produces, as the following example shows:
----
@GetMapping("/pets/{petId}", produces = ["application/json"])
@ResponseBody
fun getPet(@PathVariable String petId): Pet {
fun getPet(@PathVariable petId: String): Pet {
// ...
}
----
@@ -1674,7 +1674,7 @@ You can also use the same with request header conditions, as the following examp
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@GetMapping(path = "/pets", headers = "myHeader=myValue") // <1>
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // <1>
public void findPet(@PathVariable String petId) {
// ...
}
@@ -1684,7 +1684,7 @@ You can also use the same with request header conditions, as the following examp
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@GetMapping("/pets", headers = ["myHeader=myValue"]) // <1>
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) // <1>
fun findPet(@PathVariable petId: String) {
// ...
}
+2 -2
View File
@@ -1911,7 +1911,7 @@ You can also use the same with request header conditions, as the following examp
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
@GetMapping(path = "/pets", headers = "myHeader=myValue") // <1>
@GetMapping(path = "/pets/{petId}", headers = "myHeader=myValue") // <1>
public void findPet(@PathVariable String petId) {
// ...
}
@@ -1921,7 +1921,7 @@ You can also use the same with request header conditions, as the following examp
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
@GetMapping("/pets", headers = ["myHeader=myValue"]) // <1>
@GetMapping("/pets/{petId}", headers = ["myHeader=myValue"]) // <1>
fun findPet(@PathVariable petId: String) {
// ...
}