mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-18 00:49:02 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08bc1a050e | ||
|
|
6bfb94a563 | ||
|
|
ebc8265428 | ||
|
|
86457464d7 | ||
|
|
be129dc171 | ||
|
|
8bb1b3eb44 | ||
|
|
6abd822e77 | ||
|
|
1c43a4c7ab | ||
|
|
423f2215c2 | ||
|
|
0bad69d5fb | ||
|
|
6b19642256 | ||
|
|
19bb4e96f2 | ||
|
|
7a2594acda | ||
|
|
91c58af7af | ||
|
|
0a1aeafe08 | ||
|
|
0e69bac7b0 | ||
|
|
0cfbf6036c | ||
|
|
e931fdc5c2 | ||
|
|
3a1681357c | ||
|
|
a93382dbbf | ||
|
|
79c5ef88f5 | ||
|
|
0c80e5f9e6 | ||
|
|
7ad01a94d6 | ||
|
|
5d6d653cbd | ||
|
|
9868e888a5 | ||
|
|
6f6eb3d996 | ||
|
|
a94c50e294 | ||
|
|
b8c1255ee0 | ||
|
|
129062034b | ||
|
|
eafe3afe11 | ||
|
|
26e0343c16 | ||
|
|
4d5e7207f2 | ||
|
|
430fc25aca | ||
|
|
0882ca57d4 | ||
|
|
94bbf85c0e | ||
|
|
2c2ef12f68 | ||
|
|
120d512ff6 | ||
|
|
3ddf183922 | ||
|
|
4c69bfd32f | ||
|
|
41d71e9a7f | ||
|
|
36682b7ad2 | ||
|
|
30601a5014 | ||
|
|
f2371f5e7d | ||
|
|
f0da099b12 | ||
|
|
d00fd4c502 | ||
|
|
44a6d13cc0 | ||
|
|
284657355a | ||
|
|
6a81ed3a50 | ||
|
|
0a053cfccb | ||
|
|
22bb76d326 | ||
|
|
4dc45d551c | ||
|
|
e5d05ddfc3 | ||
|
|
44a5f8ec06 | ||
|
|
0a8bda40f4 | ||
|
|
8a879c6fed | ||
|
|
28d11aaf64 | ||
|
|
02941127e1 | ||
|
|
b1b24458c9 | ||
|
|
854b625be2 | ||
|
|
a2b7a907ec | ||
|
|
4f0a8911ca | ||
|
|
40fef7b232 | ||
|
|
423134f64c | ||
|
|
574c10d219 | ||
|
|
2b4b947050 | ||
|
|
4993b1090a | ||
|
|
3adabf391f | ||
|
|
0026338c00 | ||
|
|
cdea667e58 | ||
|
|
66b1c0b4b0 | ||
|
|
6e42d36614 | ||
|
|
7f2c93fa1f | ||
|
|
4c351e811a | ||
|
|
5b67dea506 | ||
|
|
b9fe095f60 | ||
|
|
6b17014b5a | ||
|
|
878246b09b | ||
|
|
9067ccab2d | ||
|
|
4e00aece7a | ||
|
|
c9841f37b6 | ||
|
|
92d513e9fb | ||
|
|
782ee34cb7 | ||
|
|
80c10fad92 | ||
|
|
498a0d286f | ||
|
|
e9d45d1b49 | ||
|
|
0851b0f72d | ||
|
|
91d991e86c | ||
|
|
960f6fb936 | ||
|
|
4d3d34528e | ||
|
|
78ba946266 | ||
|
|
42e7318cbb | ||
|
|
000383fbff | ||
|
|
37cbdc2cf4 | ||
|
|
6d95e7f9d9 | ||
|
|
45a7917a36 | ||
|
|
25b95b8b98 | ||
|
|
6bd7a7321e | ||
|
|
11aaba9877 | ||
|
|
07fd7606e7 | ||
|
|
40d2466334 | ||
|
|
3d6d853bbc | ||
|
|
90ea39cc6b | ||
|
|
21c3d4f4a9 | ||
|
|
de53d77344 | ||
|
|
e4e90bbec0 | ||
|
|
d0828be0cd | ||
|
|
4a4b332709 | ||
|
|
33f1c9b614 | ||
|
|
53ac812fb0 | ||
|
|
60c89dd2df | ||
|
|
4bd2531774 | ||
|
|
4fd5630700 |
@@ -28,6 +28,7 @@ buildSrc/build
|
||||
/integration-tests/build
|
||||
/src/asciidoc/build
|
||||
target/
|
||||
/target/
|
||||
|
||||
# Eclipse artifacts, including WTP generated manifests
|
||||
.classpath
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=8.0.352-librca
|
||||
java=8.0.362-librca
|
||||
|
||||
+15
-15
@@ -28,18 +28,18 @@ configure(allprojects) { project ->
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.7"
|
||||
mavenBom "io.netty:netty-bom:4.1.86.Final"
|
||||
mavenBom "io.projectreactor:reactor-bom:2020.0.27"
|
||||
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.50.v20221201"
|
||||
mavenBom "org.eclipse.jetty:jetty-bom:9.4.51.v20230217"
|
||||
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.5.32"
|
||||
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.5.2"
|
||||
mavenBom "org.jetbrains.kotlinx:kotlinx-serialization-bom:1.2.2"
|
||||
mavenBom "org.junit:junit-bom:5.8.2"
|
||||
}
|
||||
dependencies {
|
||||
dependencySet(group: 'org.apache.logging.log4j', version: '2.19.0') {
|
||||
dependencySet(group: 'org.apache.logging.log4j', version: '2.20.0') {
|
||||
entry 'log4j-api'
|
||||
entry 'log4j-core'
|
||||
entry 'log4j-jul'
|
||||
@@ -92,7 +92,7 @@ configure(allprojects) { project ->
|
||||
entry 'jibx-run'
|
||||
}
|
||||
dependency "org.ogce:xpp3:1.1.6"
|
||||
dependency "org.yaml:snakeyaml:1.30"
|
||||
dependency "org.yaml:snakeyaml:1.33"
|
||||
|
||||
dependency "com.h2database:h2:2.1.214"
|
||||
dependency "com.github.ben-manes.caffeine:caffeine:2.9.3"
|
||||
@@ -128,18 +128,18 @@ configure(allprojects) { project ->
|
||||
dependency "org.webjars:webjars-locator-core:0.48"
|
||||
dependency "org.webjars:underscorejs:1.8.3"
|
||||
|
||||
dependencySet(group: 'org.apache.tomcat', version: '9.0.70') {
|
||||
dependencySet(group: 'org.apache.tomcat', version: '9.0.73') {
|
||||
entry 'tomcat-util'
|
||||
entry('tomcat-websocket') {
|
||||
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
|
||||
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
|
||||
}
|
||||
}
|
||||
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.70') {
|
||||
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.73') {
|
||||
entry 'tomcat-embed-core'
|
||||
entry 'tomcat-embed-websocket'
|
||||
}
|
||||
dependencySet(group: 'io.undertow', version: '2.2.22.Final') {
|
||||
dependencySet(group: 'io.undertow', version: '2.2.23.Final') {
|
||||
entry 'undertow-core'
|
||||
entry('undertow-servlet') {
|
||||
exclude group: "org.jboss.spec.javax.servlet", name: "jboss-servlet-api_4.0_spec"
|
||||
@@ -191,7 +191,7 @@ configure(allprojects) { project ->
|
||||
dependency "org.junit.support:testng-engine:1.0.4"
|
||||
dependency "org.hamcrest:hamcrest:2.2"
|
||||
dependency "org.awaitility:awaitility:3.1.6"
|
||||
dependency "org.assertj:assertj-core:3.24.1"
|
||||
dependency "org.assertj:assertj-core:3.24.2"
|
||||
dependencySet(group: 'org.xmlunit', version: '2.9.0') {
|
||||
entry 'xmlunit-assertj'
|
||||
entry('xmlunit-matchers') {
|
||||
@@ -206,10 +206,10 @@ configure(allprojects) { project ->
|
||||
}
|
||||
dependency "io.mockk:mockk:1.12.1"
|
||||
|
||||
dependency("net.sourceforge.htmlunit:htmlunit:2.69.0") {
|
||||
dependency("net.sourceforge.htmlunit:htmlunit:2.70.0") {
|
||||
exclude group: "commons-logging", name: "commons-logging"
|
||||
}
|
||||
dependency("org.seleniumhq.selenium:htmlunit-driver:2.67.0") {
|
||||
dependency("org.seleniumhq.selenium:htmlunit-driver:2.70.0") {
|
||||
exclude group: "commons-logging", name: "commons-logging"
|
||||
}
|
||||
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
|
||||
@@ -340,7 +340,7 @@ configure([rootProject] + javaProjects) { project ->
|
||||
}
|
||||
|
||||
checkstyle {
|
||||
toolVersion = "10.6.0"
|
||||
toolVersion = "10.9.3"
|
||||
configDirectory.set(rootProject.file("src/checkstyle"))
|
||||
}
|
||||
|
||||
@@ -377,9 +377,9 @@ configure([rootProject] + javaProjects) { project ->
|
||||
"https://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/",
|
||||
"https://www.ehcache.org/apidocs/2.10.4/",
|
||||
"https://www.quartz-scheduler.org/api/2.3.0/",
|
||||
"https://fasterxml.github.io/jackson-core/javadoc/2.10/",
|
||||
"https://fasterxml.github.io/jackson-databind/javadoc/2.10/",
|
||||
"https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.10/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-core/2.12.7/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.core/jackson-databind/2.12.7/",
|
||||
"https://www.javadoc.io/doc/com.fasterxml.jackson.dataformat/jackson-dataformat-xml/2.12.7/",
|
||||
"https://hc.apache.org/httpcomponents-client-5.1.x/current/httpclient5/apidocs/",
|
||||
"https://projectreactor.io/docs/test/release/api/",
|
||||
"https://junit.org/junit4/javadoc/4.13.2/",
|
||||
|
||||
@@ -8,7 +8,4 @@ milestone: "5.3.x"
|
||||
build-name: "spring-framework"
|
||||
pipeline-name: "spring-framework"
|
||||
concourse-url: "https://ci.spring.io"
|
||||
registry-mirror-host: docker.repo.spring.io
|
||||
registry-mirror-username: ((artifactory-username))
|
||||
registry-mirror-password: ((artifactory-password))
|
||||
task-timeout: 1h00m
|
||||
|
||||
+11
-9
@@ -23,11 +23,6 @@ anchors:
|
||||
docker-resource-source: &docker-resource-source
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
tag: ((milestone))
|
||||
registry-mirror-vars: ®istry-mirror-vars
|
||||
registry-mirror-host: ((registry-mirror-host))
|
||||
registry-mirror-username: ((registry-mirror-username))
|
||||
registry-mirror-password: ((registry-mirror-password))
|
||||
slack-fail-params: &slack-fail-params
|
||||
text: >
|
||||
:concourse-failed: <https://ci.spring.io/teams/${BUILD_TEAM_NAME}/pipelines/${BUILD_PIPELINE_NAME}/jobs/${BUILD_JOB_NAME}/builds/${BUILD_NAME}|${BUILD_PIPELINE_NAME} ${BUILD_JOB_NAME} failed!>
|
||||
@@ -48,31 +43,37 @@ resource_types:
|
||||
- name: registry-image
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/registry-image-resource
|
||||
tag: 1.5.0
|
||||
tag: 1.7.1
|
||||
- name: artifactory-resource
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: springio/artifactory-resource
|
||||
tag: 0.0.17
|
||||
tag: 0.0.18
|
||||
- name: github-release
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: concourse/github-release-resource
|
||||
tag: 1.5.5
|
||||
tag: 1.8.0
|
||||
- name: github-status-resource
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: dpb587/github-status-resource
|
||||
tag: master
|
||||
- name: pull-request
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: teliaoss/github-pr-resource
|
||||
tag: v0.23.0
|
||||
- name: slack-notification
|
||||
type: registry-image
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: cfcommunity/slack-notification-resource
|
||||
tag: latest
|
||||
resources:
|
||||
@@ -101,6 +102,7 @@ resources:
|
||||
source:
|
||||
<<: *docker-resource-source
|
||||
repository: ((docker-hub-organization))/spring-framework-ci
|
||||
tag: ((milestone))
|
||||
- name: artifactory-repo
|
||||
type: artifactory-resource
|
||||
icon: package-variant
|
||||
@@ -168,7 +170,7 @@ jobs:
|
||||
image: ci-image
|
||||
vars:
|
||||
ci-image-name: ci-image
|
||||
<<: *registry-mirror-vars
|
||||
<<: *docker-resource-source
|
||||
- put: ci-image
|
||||
params:
|
||||
image: ci-image/image.tar
|
||||
|
||||
@@ -4,11 +4,9 @@ image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
tag: 0.9.1
|
||||
registry_mirror:
|
||||
host: ((registry-mirror-host))
|
||||
username: ((registry-mirror-username))
|
||||
password: ((registry-mirror-password))
|
||||
tag: 0.10.0
|
||||
username: ((docker-hub-username))
|
||||
password: ((docker-hub-password))
|
||||
inputs:
|
||||
- name: ci-images-git-repo
|
||||
outputs:
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
version=5.3.25-SNAPSHOT
|
||||
version=5.3.27
|
||||
org.gradle.jvmargs=-Xmx2048m
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
|
||||
+2
-2
@@ -7,8 +7,8 @@ pluginManagement {
|
||||
}
|
||||
|
||||
plugins {
|
||||
id "com.gradle.enterprise" version "3.11.1"
|
||||
id "io.spring.ge.conventions" version "0.0.11"
|
||||
id "com.gradle.enterprise" version "3.12.1"
|
||||
id "io.spring.ge.conventions" version "0.0.13"
|
||||
}
|
||||
|
||||
include "spring-aop"
|
||||
|
||||
+3
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2016 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,10 +23,6 @@ import org.springframework.lang.Nullable;
|
||||
* Subinterface of {@link org.springframework.aop.aspectj.AspectInstanceFactory}
|
||||
* that returns {@link AspectMetadata} associated with AspectJ-annotated classes.
|
||||
*
|
||||
* <p>Ideally, AspectInstanceFactory would include this method itself, but because
|
||||
* AspectMetadata uses Java-5-only {@link org.aspectj.lang.reflect.AjType},
|
||||
* we need to split out this subinterface.
|
||||
*
|
||||
* @author Rod Johnson
|
||||
* @since 2.0
|
||||
* @see AspectMetadata
|
||||
@@ -35,13 +31,13 @@ import org.springframework.lang.Nullable;
|
||||
public interface MetadataAwareAspectInstanceFactory extends AspectInstanceFactory {
|
||||
|
||||
/**
|
||||
* Return the AspectJ AspectMetadata for this factory's aspect.
|
||||
* Get the AspectJ AspectMetadata for this factory's aspect.
|
||||
* @return the aspect metadata
|
||||
*/
|
||||
AspectMetadata getAspectMetadata();
|
||||
|
||||
/**
|
||||
* Return the best possible creation mutex for this factory.
|
||||
* Get the best possible creation mutex for this factory.
|
||||
* @return the mutex object (may be {@code null} for no mutex to use)
|
||||
* @since 4.3
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* Various {@link org.springframework.aop.framework.autoproxy.TargetSourceCreator}
|
||||
* implementations for use with Spring's AOP auto-proxying support.
|
||||
*/
|
||||
@NonNullApi
|
||||
@NonNullFields
|
||||
package org.springframework.aop.framework.autoproxy.target;
|
||||
|
||||
import org.springframework.lang.NonNullApi;
|
||||
import org.springframework.lang.NonNullFields;
|
||||
@@ -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-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.
|
||||
@@ -247,7 +247,8 @@ public abstract class BeanUtils {
|
||||
// A single public constructor
|
||||
return (Constructor<T>) ctors[0];
|
||||
}
|
||||
else if (ctors.length == 0){
|
||||
else if (ctors.length == 0) {
|
||||
// No public constructors -> check non-public
|
||||
ctors = clazz.getDeclaredConstructors();
|
||||
if (ctors.length == 1) {
|
||||
// A single non-public constructor, e.g. from a non-public record type
|
||||
|
||||
+5
-3
@@ -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.Set;
|
||||
|
||||
import org.springframework.beans.SimpleTypeConverter;
|
||||
import org.springframework.beans.TypeConverter;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.config.DependencyDescriptor;
|
||||
@@ -240,10 +241,11 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa
|
||||
}
|
||||
}
|
||||
if (targetAnnotation == null) {
|
||||
BeanFactory beanFactory = getBeanFactory();
|
||||
// Look for matching annotation on the target class
|
||||
if (getBeanFactory() != null) {
|
||||
if (beanFactory != null) {
|
||||
try {
|
||||
Class<?> beanType = getBeanFactory().getType(bdHolder.getBeanName());
|
||||
Class<?> beanType = beanFactory.getType(bdHolder.getBeanName());
|
||||
if (beanType != null) {
|
||||
targetAnnotation = AnnotationUtils.getAnnotation(ClassUtils.getUserClass(beanType), type);
|
||||
}
|
||||
|
||||
+4
-3
@@ -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.
|
||||
@@ -184,8 +184,9 @@ public abstract class YamlProcessor {
|
||||
protected Yaml createYaml() {
|
||||
LoaderOptions loaderOptions = new LoaderOptions();
|
||||
loaderOptions.setAllowDuplicateKeys(false);
|
||||
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(),
|
||||
new DumperOptions(), loaderOptions);
|
||||
DumperOptions dumperOptions = new DumperOptions();
|
||||
return new Yaml(new FilteringConstructor(loaderOptions), new Representer(dumperOptions),
|
||||
dumperOptions, loaderOptions);
|
||||
}
|
||||
|
||||
private boolean process(MatchCallback callback, Yaml yaml, Resource resource) {
|
||||
|
||||
+33
-22
@@ -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.
|
||||
@@ -214,12 +214,15 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn(msg, ex);
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex);
|
||||
if (logger.isWarnEnabled()) {
|
||||
String msg = "Invocation of destroy method failed on bean with name '" + this.beanName + "'";
|
||||
if (logger.isDebugEnabled()) {
|
||||
// Log at warn level like below but add the exception stacktrace only with debug level
|
||||
logger.warn(msg, ex);
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,12 +243,15 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn(msg, ex);
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex);
|
||||
if (logger.isWarnEnabled()) {
|
||||
String msg = "Invocation of close method failed on bean with name '" + this.beanName + "'";
|
||||
if (logger.isDebugEnabled()) {
|
||||
// Log at warn level like below but add the exception stacktrace only with debug level
|
||||
logger.warn(msg, ex);
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -320,18 +326,23 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable {
|
||||
}
|
||||
}
|
||||
catch (InvocationTargetException ex) {
|
||||
String msg = "Custom destroy method '" + this.destroyMethodName + "' on bean with name '" +
|
||||
this.beanName + "' threw an exception";
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.warn(msg, ex.getTargetException());
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex.getTargetException());
|
||||
if (logger.isWarnEnabled()) {
|
||||
String msg = "Custom destroy method '" + this.destroyMethodName + "' on bean with name '" +
|
||||
this.beanName + "' threw an exception";
|
||||
if (logger.isDebugEnabled()) {
|
||||
// Log at warn level like below but add the exception stacktrace only with debug level
|
||||
logger.warn(msg, ex.getTargetException());
|
||||
}
|
||||
else {
|
||||
logger.warn(msg + ": " + ex.getTargetException());
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Throwable ex) {
|
||||
logger.warn("Failed to invoke custom destroy method '" + this.destroyMethodName +
|
||||
"' on bean with name '" + this.beanName + "'", ex);
|
||||
if (logger.isWarnEnabled()) {
|
||||
logger.warn("Failed to invoke custom destroy method '" + this.destroyMethodName +
|
||||
"' on bean with name '" + this.beanName + "'", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-2
@@ -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.
|
||||
@@ -547,7 +547,8 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader {
|
||||
* @see DefaultNamespaceHandlerResolver#DefaultNamespaceHandlerResolver(ClassLoader)
|
||||
*/
|
||||
protected NamespaceHandlerResolver createDefaultNamespaceHandlerResolver() {
|
||||
ClassLoader cl = (getResourceLoader() != null ? getResourceLoader().getClassLoader() : getBeanClassLoader());
|
||||
ResourceLoader resourceLoader = getResourceLoader();
|
||||
ClassLoader cl = (resourceLoader != null ? resourceLoader.getClassLoader() : getBeanClassLoader());
|
||||
return new DefaultNamespaceHandlerResolver(cl);
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -395,6 +395,7 @@ class BeanWrapperTests extends AbstractPropertyAccessorTests {
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("try")
|
||||
public static class ActiveResource implements AutoCloseable {
|
||||
|
||||
public ActiveResource getResource() {
|
||||
|
||||
+1
-17
@@ -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.
|
||||
@@ -436,7 +436,6 @@ class DefaultListableBeanFactoryTests {
|
||||
|
||||
@Test
|
||||
void empty() {
|
||||
ListableBeanFactory lbf = new DefaultListableBeanFactory();
|
||||
assertThat(lbf.getBeanDefinitionNames() != null).as("No beans defined --> array != null").isTrue();
|
||||
assertThat(lbf.getBeanDefinitionNames().length == 0).as("No beans defined after no arg constructor").isTrue();
|
||||
assertThat(lbf.getBeanDefinitionCount() == 0).as("No beans defined after no arg constructor").isTrue();
|
||||
@@ -778,21 +777,6 @@ class DefaultListableBeanFactoryTests {
|
||||
assertThat(factory.getType("child")).isEqualTo(DerivedTestBean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void nameAlreadyBound() {
|
||||
Properties p = new Properties();
|
||||
p.setProperty("kerry.(class)", TestBean.class.getName());
|
||||
p.setProperty("kerry.age", "35");
|
||||
registerBeanDefinitions(p);
|
||||
try {
|
||||
registerBeanDefinitions(p);
|
||||
}
|
||||
catch (BeanDefinitionStoreException ex) {
|
||||
assertThat(ex.getBeanName()).isEqualTo("kerry");
|
||||
// expected
|
||||
}
|
||||
}
|
||||
|
||||
private void singleTestBean(ListableBeanFactory lbf) {
|
||||
assertThat(lbf.getBeanDefinitionCount() == 1).as("1 beans defined").isTrue();
|
||||
String[] names = lbf.getBeanDefinitionNames();
|
||||
|
||||
Vendored
+9
-3
@@ -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.
|
||||
@@ -51,6 +51,7 @@ import org.springframework.util.ObjectUtils;
|
||||
* @author Juergen Hoeller
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @author Brian Clozel
|
||||
* @since 4.3
|
||||
* @see CaffeineCache
|
||||
*/
|
||||
@@ -188,8 +189,13 @@ public class CaffeineCacheManager implements CacheManager {
|
||||
@Override
|
||||
@Nullable
|
||||
public Cache getCache(String name) {
|
||||
return this.cacheMap.computeIfAbsent(name, cacheName ->
|
||||
this.dynamic ? createCaffeineCache(cacheName) : null);
|
||||
if (this.dynamic) {
|
||||
Cache cache = this.cacheMap.get(name);
|
||||
return (cache != null) ? cache : this.cacheMap.computeIfAbsent(name, this::createCaffeineCache);
|
||||
}
|
||||
else {
|
||||
return this.cacheMap.get(name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
+3
-3
@@ -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.
|
||||
@@ -132,10 +132,10 @@ public class JavaMailSenderImpl implements JavaMailSender {
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow {code Map} access to the JavaMail properties of this sender,
|
||||
* Allow {@code Map} access to the JavaMail properties of this sender,
|
||||
* with the option to add or override specific entries.
|
||||
* <p>Useful for specifying entries directly, for example via
|
||||
* {code javaMailProperties[mail.smtp.auth]}.
|
||||
* {@code javaMailProperties[mail.smtp.auth]}.
|
||||
*/
|
||||
public Properties getJavaMailProperties() {
|
||||
return this.javaMailProperties;
|
||||
|
||||
+6
-4
@@ -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.
|
||||
@@ -729,9 +729,11 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader
|
||||
|
||||
/**
|
||||
* Modify the application context's internal bean factory after its standard
|
||||
* initialization. All bean definitions will have been loaded, but no beans
|
||||
* will have been instantiated yet. This allows for registering special
|
||||
* BeanPostProcessors etc in certain ApplicationContext implementations.
|
||||
* initialization. The initial definition resources will have been loaded but no
|
||||
* post-processors will have run and no derived bean definitions will have been
|
||||
* registered, and most importantly, no beans will have been instantiated yet.
|
||||
* <p>This template method allows for registering special BeanPostProcessors
|
||||
* etc in certain AbstractApplicationContext subclasses.
|
||||
* @param beanFactory the bean factory used by the application context
|
||||
*/
|
||||
protected void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
+3
-3
@@ -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()
|
||||
|
||||
@@ -82,6 +82,17 @@ public abstract class AnnotationVisitor {
|
||||
this.av = annotationVisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The annotation visitor to which this visitor must delegate method calls. May be {@literal
|
||||
* null}.
|
||||
*
|
||||
* @return the annotation visitor to which this visitor must delegate method calls, or {@literal
|
||||
* null}.
|
||||
*/
|
||||
public AnnotationVisitor getDelegate() {
|
||||
return av;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits a primitive value of the annotation.
|
||||
*
|
||||
|
||||
@@ -194,7 +194,7 @@ public class ClassReader {
|
||||
this.b = classFileBuffer;
|
||||
// Check the class' major_version. This field is after the magic and minor_version fields, which
|
||||
// use 4 and 2 bytes respectively.
|
||||
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V19) {
|
||||
if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V21) {
|
||||
throw new IllegalArgumentException(
|
||||
"Unsupported class file major version " + readShort(classFileOffset + 6));
|
||||
}
|
||||
@@ -308,6 +308,7 @@ public class ClassReader {
|
||||
* @return the content of the given input stream.
|
||||
* @throws IOException if a problem occurs during reading.
|
||||
*/
|
||||
@SuppressWarnings("PMD.UseTryWithResources")
|
||||
private static byte[] readStream(final InputStream inputStream, final boolean close)
|
||||
throws IOException {
|
||||
if (inputStream == null) {
|
||||
@@ -376,7 +377,7 @@ public class ClassReader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal of name of the super class (see {@link Type#getInternalName()}). For
|
||||
* Returns the internal name of the super class (see {@link Type#getInternalName()}). For
|
||||
* interfaces, the super class is {@link Object}.
|
||||
*
|
||||
* @return the internal name of the super class, or {@literal null} for {@link Object} class.
|
||||
|
||||
@@ -42,7 +42,8 @@ public final class ClassTooLargeException extends IndexOutOfBoundsException {
|
||||
/**
|
||||
* Constructs a new {@link ClassTooLargeException}.
|
||||
*
|
||||
* @param className the internal name of the class.
|
||||
* @param className the internal name of the class (see {@link
|
||||
* org.objectweb.asm.Type#getInternalName()}).
|
||||
* @param constantPoolCount the number of constant pool items of the class.
|
||||
*/
|
||||
public ClassTooLargeException(final String className, final int constantPoolCount) {
|
||||
@@ -52,7 +53,7 @@ public final class ClassTooLargeException extends IndexOutOfBoundsException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the internal name of the class.
|
||||
* Returns the internal name of the class (see {@link org.objectweb.asm.Type#getInternalName()}).
|
||||
*
|
||||
* @return the internal name of the class.
|
||||
*/
|
||||
|
||||
@@ -81,6 +81,15 @@ public abstract class ClassVisitor {
|
||||
this.cv = classVisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The class visitor to which this visitor must delegate method calls. May be {@literal null}.
|
||||
*
|
||||
* @return the class visitor to which this visitor must delegate method calls, or {@literal null}.
|
||||
*/
|
||||
public ClassVisitor getDelegate() {
|
||||
return cv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits the header of the class.
|
||||
*
|
||||
@@ -155,7 +164,8 @@ public abstract class ClassVisitor {
|
||||
* implicitly its own nest, so it's invalid to call this method with the visited class name as
|
||||
* argument.
|
||||
*
|
||||
* @param nestHost the internal name of the host class of the nest.
|
||||
* @param nestHost the internal name of the host class of the nest (see {@link
|
||||
* Type#getInternalName()}).
|
||||
*/
|
||||
public void visitNestHost(final String nestHost) {
|
||||
if (api < Opcodes.ASM7) {
|
||||
@@ -167,14 +177,19 @@ public abstract class ClassVisitor {
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits the enclosing class of the class. This method must be called only if the class has an
|
||||
* enclosing class.
|
||||
* Visits the enclosing class of the class. This method must be called only if this class is a
|
||||
* local or anonymous class. See the JVMS 4.7.7 section for more details.
|
||||
*
|
||||
* @param owner internal name of the enclosing class of the class.
|
||||
* @param owner internal name of the enclosing class of the class (see {@link
|
||||
* Type#getInternalName()}).
|
||||
* @param name the name of the method that contains the class, or {@literal null} if the class is
|
||||
* not enclosed in a method of its enclosing class.
|
||||
* not enclosed in a method or constructor of its enclosing class (e.g. if it is enclosed in
|
||||
* an instance initializer, static initializer, instance variable initializer, or class
|
||||
* variable initializer).
|
||||
* @param descriptor the descriptor of the method that contains the class, or {@literal null} if
|
||||
* the class is not enclosed in a method of its enclosing class.
|
||||
* the class is not enclosed in a method or constructor of its enclosing class (e.g. if it is
|
||||
* enclosed in an instance initializer, static initializer, instance variable initializer, or
|
||||
* class variable initializer).
|
||||
*/
|
||||
public void visitOuterClass(final String owner, final String name, final String descriptor) {
|
||||
if (cv != null) {
|
||||
@@ -241,7 +256,7 @@ public abstract class ClassVisitor {
|
||||
* the visited class is the host of a nest. A nest host is implicitly a member of its own nest, so
|
||||
* it's invalid to call this method with the visited class name as argument.
|
||||
*
|
||||
* @param nestMember the internal name of a nest member.
|
||||
* @param nestMember the internal name of a nest member (see {@link Type#getInternalName()}).
|
||||
*/
|
||||
public void visitNestMember(final String nestMember) {
|
||||
if (api < Opcodes.ASM7) {
|
||||
@@ -256,7 +271,8 @@ public abstract class ClassVisitor {
|
||||
* Visits a permitted subclasses. A permitted subclass is one of the allowed subclasses of the
|
||||
* current class.
|
||||
*
|
||||
* @param permittedSubclass the internal name of a permitted subclass.
|
||||
* @param permittedSubclass the internal name of a permitted subclass (see {@link
|
||||
* Type#getInternalName()}).
|
||||
*/
|
||||
public void visitPermittedSubclass(final String permittedSubclass) {
|
||||
if (api < Opcodes.ASM9) {
|
||||
@@ -269,15 +285,18 @@ public abstract class ClassVisitor {
|
||||
|
||||
/**
|
||||
* Visits information about an inner class. This inner class is not necessarily a member of the
|
||||
* class being visited.
|
||||
* class being visited. More precisely, every class or interface C which is referenced by this
|
||||
* class and which is not a package member must be visited with this method. This class must
|
||||
* reference its nested class or interface members, and its enclosing class, if any. See the JVMS
|
||||
* 4.7.6 section for more details.
|
||||
*
|
||||
* @param name the internal name of an inner class (see {@link Type#getInternalName()}).
|
||||
* @param outerName the internal name of the class to which the inner class belongs (see {@link
|
||||
* Type#getInternalName()}). May be {@literal null} for not member classes.
|
||||
* @param innerName the (simple) name of the inner class inside its enclosing class. May be
|
||||
* {@literal null} for anonymous inner classes.
|
||||
* @param access the access flags of the inner class as originally declared in the enclosing
|
||||
* class.
|
||||
* @param name the internal name of C (see {@link Type#getInternalName()}).
|
||||
* @param outerName the internal name of the class or interface C is a member of (see {@link
|
||||
* Type#getInternalName()}). Must be {@literal null} if C is not the member of a class or
|
||||
* interface (e.g. for local or anonymous classes).
|
||||
* @param innerName the (simple) name of C. Must be {@literal null} for anonymous inner classes.
|
||||
* @param access the access flags of C originally declared in the source code from which this
|
||||
* class was compiled.
|
||||
*/
|
||||
public void visitInnerClass(
|
||||
final String name, final String outerName, final String innerName, final int access) {
|
||||
|
||||
@@ -842,7 +842,7 @@ public class ClassWriter extends ClassVisitor {
|
||||
* constant pool already contains a similar item. <i>This method is intended for {@link Attribute}
|
||||
* sub classes, and is normally not needed by class generators or adapters.</i>
|
||||
*
|
||||
* @param value the internal name of the class.
|
||||
* @param value the internal name of the class (see {@link Type#getInternalName()}).
|
||||
* @return the index of a new or already existing class reference item.
|
||||
*/
|
||||
public int newClass(final String value) {
|
||||
@@ -894,7 +894,8 @@ public class ClassWriter extends ClassVisitor {
|
||||
* Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link
|
||||
* Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL},
|
||||
* {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}.
|
||||
* @param owner the internal name of the field or method owner class.
|
||||
* @param owner the internal name of the field or method owner class (see {@link
|
||||
* Type#getInternalName()}).
|
||||
* @param name the name of the field or method.
|
||||
* @param descriptor the descriptor of the field or method.
|
||||
* @return the index of a new or already existing method type reference item.
|
||||
@@ -916,7 +917,8 @@ public class ClassWriter extends ClassVisitor {
|
||||
* Opcodes#H_GETSTATIC}, {@link Opcodes#H_PUTFIELD}, {@link Opcodes#H_PUTSTATIC}, {@link
|
||||
* Opcodes#H_INVOKEVIRTUAL}, {@link Opcodes#H_INVOKESTATIC}, {@link Opcodes#H_INVOKESPECIAL},
|
||||
* {@link Opcodes#H_NEWINVOKESPECIAL} or {@link Opcodes#H_INVOKEINTERFACE}.
|
||||
* @param owner the internal name of the field or method owner class.
|
||||
* @param owner the internal name of the field or method owner class (see {@link
|
||||
* Type#getInternalName()}).
|
||||
* @param name the name of the field or method.
|
||||
* @param descriptor the descriptor of the field or method.
|
||||
* @param isInterface true if the owner is an interface.
|
||||
@@ -978,7 +980,7 @@ public class ClassWriter extends ClassVisitor {
|
||||
* constant pool already contains a similar item. <i>This method is intended for {@link Attribute}
|
||||
* sub classes, and is normally not needed by class generators or adapters.</i>
|
||||
*
|
||||
* @param owner the internal name of the field's owner class.
|
||||
* @param owner the internal name of the field's owner class (see {@link Type#getInternalName()}).
|
||||
* @param name the field's name.
|
||||
* @param descriptor the field's descriptor.
|
||||
* @return the index of a new or already existing field reference item.
|
||||
@@ -992,7 +994,8 @@ public class ClassWriter extends ClassVisitor {
|
||||
* constant pool already contains a similar item. <i>This method is intended for {@link Attribute}
|
||||
* sub classes, and is normally not needed by class generators or adapters.</i>
|
||||
*
|
||||
* @param owner the internal name of the method's owner class.
|
||||
* @param owner the internal name of the method's owner class (see {@link
|
||||
* Type#getInternalName()}).
|
||||
* @param name the method's name.
|
||||
* @param descriptor the method's descriptor.
|
||||
* @param isInterface {@literal true} if {@code owner} is an interface.
|
||||
@@ -1028,9 +1031,10 @@ public class ClassWriter extends ClassVisitor {
|
||||
* currently being generated by this ClassWriter, which can of course not be loaded since it is
|
||||
* under construction.
|
||||
*
|
||||
* @param type1 the internal name of a class.
|
||||
* @param type2 the internal name of another class.
|
||||
* @return the internal name of the common super class of the two given classes.
|
||||
* @param type1 the internal name of a class (see {@link Type#getInternalName()}).
|
||||
* @param type2 the internal name of another class (see {@link Type#getInternalName()}).
|
||||
* @return the internal name of the common super class of the two given classes (see {@link
|
||||
* Type#getInternalName()}).
|
||||
*/
|
||||
protected String getCommonSuperClass(final String type1, final String type2) {
|
||||
ClassLoader classLoader = getClassLoader();
|
||||
|
||||
@@ -78,6 +78,15 @@ public abstract class FieldVisitor {
|
||||
this.fv = fieldVisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The field visitor to which this visitor must delegate method calls. May be {@literal null}.
|
||||
*
|
||||
* @return the field visitor to which this visitor must delegate method calls, or {@literal null}.
|
||||
*/
|
||||
public FieldVisitor getDelegate() {
|
||||
return fv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visits an annotation of the field.
|
||||
*
|
||||
|
||||
@@ -367,11 +367,12 @@ class Frame {
|
||||
typeValue = REFERENCE_KIND | symbolTable.addType(internalName);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
throw new IllegalArgumentException(
|
||||
"Invalid descriptor fragment: " + buffer.substring(elementDescriptorOffset));
|
||||
}
|
||||
return ((elementDescriptorOffset - offset) << DIM_SHIFT) | typeValue;
|
||||
default:
|
||||
throw new IllegalArgumentException();
|
||||
throw new IllegalArgumentException("Invalid descriptor: " + buffer.substring(offset));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ public final class Handle {
|
||||
* {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link
|
||||
* Opcodes#H_INVOKEINTERFACE}.
|
||||
* @param owner the internal name of the class that owns the field or method designated by this
|
||||
* handle.
|
||||
* handle (see {@link Type#getInternalName()}).
|
||||
* @param name the name of the field or method designated by this handle.
|
||||
* @param descriptor the descriptor of the field or method designated by this handle.
|
||||
* @deprecated this constructor has been superseded by {@link #Handle(int, String, String, String,
|
||||
@@ -85,7 +85,7 @@ public final class Handle {
|
||||
* {@link Opcodes#H_INVOKESPECIAL}, {@link Opcodes#H_NEWINVOKESPECIAL} or {@link
|
||||
* Opcodes#H_INVOKEINTERFACE}.
|
||||
* @param owner the internal name of the class that owns the field or method designated by this
|
||||
* handle.
|
||||
* handle (see {@link Type#getInternalName()}).
|
||||
* @param name the name of the field or method designated by this handle.
|
||||
* @param descriptor the descriptor of the field or method designated by this handle.
|
||||
* @param isInterface whether the owner is an interface or not.
|
||||
@@ -118,7 +118,8 @@ public final class Handle {
|
||||
/**
|
||||
* Returns the internal name of the class that owns the field or method designated by this handle.
|
||||
*
|
||||
* @return the internal name of the class that owns the field or method designated by this handle.
|
||||
* @return the internal name of the class that owns the field or method designated by this handle
|
||||
* (see {@link Type#getInternalName()}).
|
||||
*/
|
||||
public String getOwner() {
|
||||
return owner;
|
||||
|
||||
@@ -44,7 +44,7 @@ public final class MethodTooLargeException extends IndexOutOfBoundsException {
|
||||
/**
|
||||
* Constructs a new {@link MethodTooLargeException}.
|
||||
*
|
||||
* @param className the internal name of the owner class.
|
||||
* @param className the internal name of the owner class (see {@link Type#getInternalName()}).
|
||||
* @param methodName the name of the method.
|
||||
* @param descriptor the descriptor of the method.
|
||||
* @param codeSize the size of the method's Code attribute, in bytes.
|
||||
@@ -64,7 +64,7 @@ public final class MethodTooLargeException extends IndexOutOfBoundsException {
|
||||
/**
|
||||
* Returns the internal name of the owner class.
|
||||
*
|
||||
* @return the internal name of the owner class.
|
||||
* @return the internal name of the owner class (see {@link Type#getInternalName()}).
|
||||
*/
|
||||
public String getClassName() {
|
||||
return className;
|
||||
|
||||
@@ -94,6 +94,16 @@ public abstract class MethodVisitor {
|
||||
this.mv = methodVisitor;
|
||||
}
|
||||
|
||||
/**
|
||||
* The method visitor to which this visitor must delegate method calls. May be {@literal null}.
|
||||
*
|
||||
* @return the method visitor to which this visitor must delegate method calls, or {@literal
|
||||
* null}.
|
||||
*/
|
||||
public MethodVisitor getDelegate() {
|
||||
return mv;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
// Parameters, annotations and non standard attributes
|
||||
// -----------------------------------------------------------------------------------------------
|
||||
@@ -120,7 +130,7 @@ public abstract class MethodVisitor {
|
||||
* @return a visitor to the visit the actual default value of this annotation interface method, or
|
||||
* {@literal null} if this visitor is not interested in visiting this default value. The
|
||||
* 'name' parameters passed to the methods of this annotation visitor are ignored. Moreover,
|
||||
* exacly one visit method must be called on this annotation visitor, followed by visitEnd.
|
||||
* exactly one visit method must be called on this annotation visitor, followed by visitEnd.
|
||||
*/
|
||||
public AnnotationVisitor visitAnnotationDefault() {
|
||||
if (mv != null) {
|
||||
@@ -273,15 +283,17 @@ public abstract class MethodVisitor {
|
||||
* @param type the type of this stack map frame. Must be {@link Opcodes#F_NEW} for expanded
|
||||
* frames, or {@link Opcodes#F_FULL}, {@link Opcodes#F_APPEND}, {@link Opcodes#F_CHOP}, {@link
|
||||
* Opcodes#F_SAME} or {@link Opcodes#F_APPEND}, {@link Opcodes#F_SAME1} for compressed frames.
|
||||
* @param numLocal the number of local variables in the visited frame.
|
||||
* @param numLocal the number of local variables in the visited frame. Long and double values
|
||||
* count for one variable.
|
||||
* @param local the local variable types in this frame. This array must not be modified. Primitive
|
||||
* types are represented by {@link Opcodes#TOP}, {@link Opcodes#INTEGER}, {@link
|
||||
* Opcodes#FLOAT}, {@link Opcodes#LONG}, {@link Opcodes#DOUBLE}, {@link Opcodes#NULL} or
|
||||
* {@link Opcodes#UNINITIALIZED_THIS} (long and double are represented by a single element).
|
||||
* Reference types are represented by String objects (representing internal names), and
|
||||
* uninitialized types by Label objects (this label designates the NEW instruction that
|
||||
* created this uninitialized value).
|
||||
* @param numStack the number of operand stack elements in the visited frame.
|
||||
* Reference types are represented by String objects (representing internal names, see {@link
|
||||
* Type#getInternalName()}), and uninitialized types by Label objects (this label designates
|
||||
* the NEW instruction that created this uninitialized value).
|
||||
* @param numStack the number of operand stack elements in the visited frame. Long and double
|
||||
* values count for one stack element.
|
||||
* @param stack the operand stack types in this frame. This array must not be modified. Its
|
||||
* content has the same format as the "local" array.
|
||||
* @throws IllegalStateException if a frame is visited just after another one, without any
|
||||
@@ -360,7 +372,7 @@ public abstract class MethodVisitor {
|
||||
|
||||
/**
|
||||
* Visits a type instruction. A type instruction is an instruction that takes the internal name of
|
||||
* a class as parameter.
|
||||
* a class as parameter (see {@link Type#getInternalName()}).
|
||||
*
|
||||
* @param opcode the opcode of the type instruction to be visited. This opcode is either NEW,
|
||||
* ANEWARRAY, CHECKCAST or INSTANCEOF.
|
||||
@@ -552,12 +564,12 @@ public abstract class MethodVisitor {
|
||||
/**
|
||||
* Visits an IINC instruction.
|
||||
*
|
||||
* @param var index of the local variable to be incremented.
|
||||
* @param varIndex index of the local variable to be incremented.
|
||||
* @param increment amount to increment the local variable by.
|
||||
*/
|
||||
public void visitIincInsn(final int var, final int increment) {
|
||||
public void visitIincInsn(final int varIndex, final int increment) {
|
||||
if (mv != null) {
|
||||
mv.visitIincInsn(var, increment);
|
||||
mv.visitIincInsn(varIndex, increment);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -643,8 +655,9 @@ public abstract class MethodVisitor {
|
||||
* @param start the beginning of the exception handler's scope (inclusive).
|
||||
* @param end the end of the exception handler's scope (exclusive).
|
||||
* @param handler the beginning of the exception handler's code.
|
||||
* @param type the internal name of the type of exceptions handled by the handler, or {@literal
|
||||
* null} to catch any exceptions (for "finally" blocks).
|
||||
* @param type the internal name of the type of exceptions handled by the handler (see {@link
|
||||
* Type#getInternalName()}), or {@literal null} to catch any exceptions (for "finally"
|
||||
* blocks).
|
||||
* @throws IllegalArgumentException if one of the labels has already been visited by this visitor
|
||||
* (by the {@link #visitLabel} method).
|
||||
*/
|
||||
|
||||
@@ -284,6 +284,8 @@ public interface Opcodes {
|
||||
int V17 = 0 << 16 | 61;
|
||||
int V18 = 0 << 16 | 62;
|
||||
int V19 = 0 << 16 | 63;
|
||||
int V20 = 0 << 16 | 64;
|
||||
int V21 = 0 << 16 | 65;
|
||||
|
||||
/**
|
||||
* Version flag indicating that the class is using 'preview' features.
|
||||
|
||||
@@ -45,7 +45,7 @@ public abstract class RecordComponentVisitor {
|
||||
/**
|
||||
* The record visitor to which this visitor must delegate method calls. May be {@literal null}.
|
||||
*/
|
||||
/*package-private*/ RecordComponentVisitor delegate;
|
||||
protected RecordComponentVisitor delegate;
|
||||
|
||||
/**
|
||||
* Constructs a new {@link RecordComponentVisitor}.
|
||||
@@ -83,7 +83,8 @@ public abstract class RecordComponentVisitor {
|
||||
/**
|
||||
* The record visitor to which this visitor must delegate method calls. May be {@literal null}.
|
||||
*
|
||||
* @return the record visitor to which this visitor must delegate method calls or {@literal null}.
|
||||
* @return the record visitor to which this visitor must delegate method calls, or {@literal
|
||||
* null}.
|
||||
*/
|
||||
public RecordComponentVisitor getDelegate() {
|
||||
return delegate;
|
||||
|
||||
@@ -37,7 +37,7 @@ final class RecordComponentWriter extends RecordComponentVisitor {
|
||||
/** The name_index field of the Record attribute. */
|
||||
private final int nameIndex;
|
||||
|
||||
/** The descriptor_index field of the the Record attribute. */
|
||||
/** The descriptor_index field of the Record attribute. */
|
||||
private final int descriptorIndex;
|
||||
|
||||
/**
|
||||
|
||||
@@ -245,7 +245,7 @@ public final class Type {
|
||||
/**
|
||||
* Returns the {@link Type} corresponding to the given internal name.
|
||||
*
|
||||
* @param internalName an internal name.
|
||||
* @param internalName an internal name (see {@link Type#getInternalName()}).
|
||||
* @return the {@link Type} corresponding to the given internal name.
|
||||
*/
|
||||
public static Type getObjectType(final String internalName) {
|
||||
@@ -708,8 +708,8 @@ public final class Type {
|
||||
*
|
||||
* @return the size of the arguments of the method (plus one for the implicit this argument),
|
||||
* argumentsSize, and the size of its return value, returnSize, packed into a single int i =
|
||||
* {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code
|
||||
* i >> 2}, and returnSize to {@code i & 0x03}).
|
||||
* {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code
|
||||
* i >> 2}, and returnSize to {@code i & 0x03}).
|
||||
*/
|
||||
public int getArgumentsAndReturnSizes() {
|
||||
return getArgumentsAndReturnSizes(getDescriptor());
|
||||
@@ -721,8 +721,8 @@ public final class Type {
|
||||
* @param methodDescriptor a method descriptor.
|
||||
* @return the size of the arguments of the method (plus one for the implicit this argument),
|
||||
* argumentsSize, and the size of its return value, returnSize, packed into a single int i =
|
||||
* {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code
|
||||
* i >> 2}, and returnSize to {@code i & 0x03}).
|
||||
* {@code (argumentsSize << 2) | returnSize} (argumentsSize is therefore equal to {@code
|
||||
* i >> 2}, and returnSize to {@code i & 0x03}).
|
||||
*/
|
||||
public static int getArgumentsAndReturnSizes(final String methodDescriptor) {
|
||||
int argumentsSize = 1;
|
||||
|
||||
+2
-2
@@ -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-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.
|
||||
@@ -22,12 +22,13 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* A simple log message type for use with Commons Logging, allowing
|
||||
* for convenient lazy resolution of a given {@link Supplier} instance
|
||||
* (typically bound to a Java 8 lambda expression) or a printf-style
|
||||
* format string ({@link String#format}) in its {@link #toString()}.
|
||||
* A simple log message type for use with Commons Logging, allowing for convenient
|
||||
* lazy resolution of a given {@link Supplier} instance (typically bound to a lambda
|
||||
* expression) or a printf-style format string ({@link String#format}) in its
|
||||
* {@link #toString()}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.2
|
||||
* @see #of(Supplier)
|
||||
* @see #format(String, Object)
|
||||
@@ -77,7 +78,7 @@ public abstract class LogMessage implements CharSequence {
|
||||
|
||||
/**
|
||||
* Build a lazily resolving message from the given supplier.
|
||||
* @param supplier the supplier (typically bound to a Java 8 lambda expression)
|
||||
* @param supplier the supplier (typically bound to a lambda expression)
|
||||
* @see #toString()
|
||||
*/
|
||||
public static LogMessage of(Supplier<? extends CharSequence> supplier) {
|
||||
@@ -87,63 +88,68 @@ public abstract class LogMessage implements CharSequence {
|
||||
/**
|
||||
* Build a lazily formatted message from the given format string and argument.
|
||||
* @param format the format string (following {@link String#format} rules)
|
||||
* @param arg1 the argument
|
||||
* @param arg1 the argument (can be {@code null})
|
||||
* @see String#format(String, Object...)
|
||||
*/
|
||||
public static LogMessage format(String format, Object arg1) {
|
||||
public static LogMessage format(String format, @Nullable Object arg1) {
|
||||
return new FormatMessage1(format, arg1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lazily formatted message from the given format string and arguments.
|
||||
* @param format the format string (following {@link String#format} rules)
|
||||
* @param arg1 the first argument
|
||||
* @param arg2 the second argument
|
||||
* @param arg1 the first argument (can be {@code null})
|
||||
* @param arg2 the second argument (can be {@code null})
|
||||
* @see String#format(String, Object...)
|
||||
*/
|
||||
public static LogMessage format(String format, Object arg1, Object arg2) {
|
||||
public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2) {
|
||||
return new FormatMessage2(format, arg1, arg2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lazily formatted message from the given format string and arguments.
|
||||
* @param format the format string (following {@link String#format} rules)
|
||||
* @param arg1 the first argument
|
||||
* @param arg2 the second argument
|
||||
* @param arg3 the third argument
|
||||
* @param arg1 the first argument (can be {@code null})
|
||||
* @param arg2 the second argument (can be {@code null})
|
||||
* @param arg3 the third argument (can be {@code null})
|
||||
* @see String#format(String, Object...)
|
||||
*/
|
||||
public static LogMessage format(String format, Object arg1, Object arg2, Object arg3) {
|
||||
public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
|
||||
return new FormatMessage3(format, arg1, arg2, arg3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lazily formatted message from the given format string and arguments.
|
||||
* @param format the format string (following {@link String#format} rules)
|
||||
* @param arg1 the first argument
|
||||
* @param arg2 the second argument
|
||||
* @param arg3 the third argument
|
||||
* @param arg4 the fourth argument
|
||||
* @param arg1 the first argument (can be {@code null})
|
||||
* @param arg2 the second argument (can be {@code null})
|
||||
* @param arg3 the third argument (can be {@code null})
|
||||
* @param arg4 the fourth argument (can be {@code null})
|
||||
* @see String#format(String, Object...)
|
||||
*/
|
||||
public static LogMessage format(String format, Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
public static LogMessage format(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3,
|
||||
@Nullable Object arg4) {
|
||||
return new FormatMessage4(format, arg1, arg2, arg3, arg4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a lazily formatted message from the given format string and varargs.
|
||||
* <p>This varargs {@code format()} variant may be costly. You should therefore
|
||||
* use the individual argument variants whenever possible:
|
||||
* {@link #format(String, Object)}, {@link #format(String, Object, Object)}, etc.
|
||||
* @param format the format string (following {@link String#format} rules)
|
||||
* @param args the varargs array (costly, prefer individual arguments)
|
||||
* @param args the varargs array (can be {@code null} and can contain {@code null}
|
||||
* elements)
|
||||
* @see String#format(String, Object...)
|
||||
*/
|
||||
public static LogMessage format(String format, Object... args) {
|
||||
public static LogMessage format(String format, @Nullable Object... args) {
|
||||
return new FormatMessageX(format, args);
|
||||
}
|
||||
|
||||
|
||||
private static final class SupplierMessage extends LogMessage {
|
||||
|
||||
private Supplier<? extends CharSequence> supplier;
|
||||
private final Supplier<? extends CharSequence> supplier;
|
||||
|
||||
SupplierMessage(Supplier<? extends CharSequence> supplier) {
|
||||
Assert.notNull(supplier, "Supplier must not be null");
|
||||
@@ -170,9 +176,10 @@ public abstract class LogMessage implements CharSequence {
|
||||
|
||||
private static final class FormatMessage1 extends FormatMessage {
|
||||
|
||||
@Nullable
|
||||
private final Object arg1;
|
||||
|
||||
FormatMessage1(String format, Object arg1) {
|
||||
FormatMessage1(String format, @Nullable Object arg1) {
|
||||
super(format);
|
||||
this.arg1 = arg1;
|
||||
}
|
||||
@@ -186,11 +193,13 @@ public abstract class LogMessage implements CharSequence {
|
||||
|
||||
private static final class FormatMessage2 extends FormatMessage {
|
||||
|
||||
@Nullable
|
||||
private final Object arg1;
|
||||
|
||||
@Nullable
|
||||
private final Object arg2;
|
||||
|
||||
FormatMessage2(String format, Object arg1, Object arg2) {
|
||||
FormatMessage2(String format, @Nullable Object arg1, @Nullable Object arg2) {
|
||||
super(format);
|
||||
this.arg1 = arg1;
|
||||
this.arg2 = arg2;
|
||||
@@ -205,13 +214,16 @@ public abstract class LogMessage implements CharSequence {
|
||||
|
||||
private static final class FormatMessage3 extends FormatMessage {
|
||||
|
||||
@Nullable
|
||||
private final Object arg1;
|
||||
|
||||
@Nullable
|
||||
private final Object arg2;
|
||||
|
||||
@Nullable
|
||||
private final Object arg3;
|
||||
|
||||
FormatMessage3(String format, Object arg1, Object arg2, Object arg3) {
|
||||
FormatMessage3(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) {
|
||||
super(format);
|
||||
this.arg1 = arg1;
|
||||
this.arg2 = arg2;
|
||||
@@ -227,15 +239,20 @@ public abstract class LogMessage implements CharSequence {
|
||||
|
||||
private static final class FormatMessage4 extends FormatMessage {
|
||||
|
||||
@Nullable
|
||||
private final Object arg1;
|
||||
|
||||
@Nullable
|
||||
private final Object arg2;
|
||||
|
||||
@Nullable
|
||||
private final Object arg3;
|
||||
|
||||
@Nullable
|
||||
private final Object arg4;
|
||||
|
||||
FormatMessage4(String format, Object arg1, Object arg2, Object arg3, Object arg4) {
|
||||
FormatMessage4(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3,
|
||||
@Nullable Object arg4) {
|
||||
super(format);
|
||||
this.arg1 = arg1;
|
||||
this.arg2 = arg2;
|
||||
@@ -252,9 +269,10 @@ public abstract class LogMessage implements CharSequence {
|
||||
|
||||
private static final class FormatMessageX extends FormatMessage {
|
||||
|
||||
@Nullable
|
||||
private final Object[] args;
|
||||
|
||||
FormatMessageX(String format, Object... args) {
|
||||
FormatMessageX(String format, @Nullable Object... args) {
|
||||
super(format);
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
+6
-1
@@ -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.
|
||||
@@ -90,4 +90,9 @@ public class ExecutorServiceAdapter extends AbstractExecutorService {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @Override on JDK 19
|
||||
public void close() {
|
||||
// no-op in order to avoid container-triggered shutdown call which would lead to exception logging
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -64,7 +64,7 @@ public class InstanceFilter<T> {
|
||||
|
||||
|
||||
/**
|
||||
* Determine if the specified {code instance} matches this filter.
|
||||
* Determine if the specified {@code instance} matches this filter.
|
||||
*/
|
||||
public boolean match(T instance) {
|
||||
Assert.notNull(instance, "Instance to match must not be null");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+39
-6
@@ -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} — 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
|
||||
|
||||
+29
-4
@@ -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);
|
||||
}
|
||||
|
||||
+22
-2
@@ -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.
|
||||
@@ -264,7 +264,27 @@ public enum SpelMessage {
|
||||
|
||||
/** @since 5.3.17 */
|
||||
MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED(Kind.ERROR, 1075,
|
||||
"Array declares too many elements, exceeding the threshold of ''{0}''");
|
||||
"Array declares too many elements, exceeding the threshold of ''{0}''"),
|
||||
|
||||
/** @since 5.2.23 */
|
||||
MAX_REPEATED_TEXT_SIZE_EXCEEDED(Kind.ERROR, 1076,
|
||||
"Repeated text is too long, exceeding the threshold of ''{0}'' characters"),
|
||||
|
||||
/** @since 5.2.23 */
|
||||
MAX_REGEX_LENGTH_EXCEEDED(Kind.ERROR, 1077,
|
||||
"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
|
||||
|
||||
+9
-4
@@ -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
|
||||
|
||||
+2
-2
@@ -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.
|
||||
@@ -144,7 +144,7 @@ public class FunctionReference extends SpelNodeImpl {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
sj.add(getChild(i).toStringAST());
|
||||
}
|
||||
return '#' + this.name + sj.toString();
|
||||
return '#' + this.name + sj;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+10
-4
@@ -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
|
||||
|
||||
+4
-7
@@ -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.
|
||||
@@ -263,7 +263,7 @@ public class MethodReference extends SpelNodeImpl {
|
||||
for (int i = 0; i < getChildCount(); i++) {
|
||||
sj.add(getChild(i).toStringAST());
|
||||
}
|
||||
return this.name + sj.toString();
|
||||
return this.name + sj;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,12 +288,9 @@ public class MethodReference extends SpelNodeImpl {
|
||||
if (executor.didArgumentConversionOccur()) {
|
||||
return false;
|
||||
}
|
||||
Class<?> clazz = executor.getMethod().getDeclaringClass();
|
||||
if (!Modifier.isPublic(clazz.getModifiers()) && executor.getPublicDeclaringClass() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
Class<?> clazz = executor.getMethod().getDeclaringClass();
|
||||
return (Modifier.isPublic(clazz.getModifiers()) || executor.getPublicDeclaringClass() != null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+24
-5
@@ -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.
|
||||
@@ -25,6 +25,8 @@ import org.springframework.expression.Operation;
|
||||
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.util.Assert;
|
||||
import org.springframework.util.NumberUtils;
|
||||
|
||||
@@ -52,6 +54,13 @@ import org.springframework.util.NumberUtils;
|
||||
*/
|
||||
public class OpMultiply extends Operator {
|
||||
|
||||
/**
|
||||
* Maximum number of characters permitted in repeated text.
|
||||
* @since 5.2.23
|
||||
*/
|
||||
private static final int MAX_REPEATED_TEXT_SIZE = 256;
|
||||
|
||||
|
||||
public OpMultiply(int startPos, int endPos, SpelNodeImpl... operands) {
|
||||
super("*", startPos, endPos, operands);
|
||||
}
|
||||
@@ -109,10 +118,13 @@ public class OpMultiply extends Operator {
|
||||
}
|
||||
|
||||
if (leftOperand instanceof String && rightOperand instanceof Integer) {
|
||||
int repeats = (Integer) rightOperand;
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (int i = 0; i < repeats; i++) {
|
||||
result.append(leftOperand);
|
||||
String text = (String) leftOperand;
|
||||
int count = (Integer) rightOperand;
|
||||
int requestedSize = text.length() * count;
|
||||
checkRepeatedTextSize(requestedSize);
|
||||
StringBuilder result = new StringBuilder(requestedSize);
|
||||
for (int i = 0; i < count; i++) {
|
||||
result.append(text);
|
||||
}
|
||||
return new TypedValue(result.toString());
|
||||
}
|
||||
@@ -120,6 +132,13 @@ public class OpMultiply extends Operator {
|
||||
return state.operate(Operation.MULTIPLY, leftOperand, rightOperand);
|
||||
}
|
||||
|
||||
private void checkRepeatedTextSize(int requestedSize) {
|
||||
if (requestedSize > MAX_REPEATED_TEXT_SIZE) {
|
||||
throw new SpelEvaluationException(getStartPosition(),
|
||||
SpelMessage.MAX_REPEATED_TEXT_SIZE_EXCEEDED, MAX_REPEATED_TEXT_SIZE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCompilable() {
|
||||
if (!getLeftOperand().isCompilable()) {
|
||||
|
||||
+39
-6
@@ -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
|
||||
|
||||
+43
-12
@@ -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.
|
||||
@@ -36,19 +36,41 @@ import org.springframework.expression.spel.support.BooleanTypedValue;
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
*/
|
||||
public class OperatorMatches extends Operator {
|
||||
|
||||
private static final int PATTERN_ACCESS_THRESHOLD = 1000000;
|
||||
|
||||
private final ConcurrentMap<String, Pattern> patternCache = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* Maximum number of characters permitted in a regular expression.
|
||||
* @since 5.2.23
|
||||
*/
|
||||
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.2.23 in favor of invoking
|
||||
* {@link #OperatorMatches(ConcurrentMap, int, int, SpelNodeImpl...)}
|
||||
* with a shared pattern cache instead
|
||||
*/
|
||||
@Deprecated
|
||||
public OperatorMatches(int startPos, int endPos, SpelNodeImpl... operands) {
|
||||
super("matches", startPos, endPos, operands);
|
||||
this(new ConcurrentHashMap<>(), startPos, endPos, operands);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link OperatorMatches} instance with a shared pattern cache.
|
||||
* @since 5.2.23
|
||||
*/
|
||||
public OperatorMatches(ConcurrentMap<String, Pattern> patternCache, int startPos, int endPos, SpelNodeImpl... operands) {
|
||||
super("matches", startPos, endPos, operands);
|
||||
this.patternCache = patternCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the first operand matches the regex specified as the second operand.
|
||||
@@ -62,26 +84,28 @@ public class OperatorMatches extends Operator {
|
||||
public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
|
||||
SpelNodeImpl leftOp = getLeftOperand();
|
||||
SpelNodeImpl rightOp = getRightOperand();
|
||||
String left = leftOp.getValue(state, String.class);
|
||||
Object right = getRightOperand().getValue(state);
|
||||
|
||||
if (left == null) {
|
||||
String input = leftOp.getValue(state, String.class);
|
||||
if (input == null) {
|
||||
throw new SpelEvaluationException(leftOp.getStartPosition(),
|
||||
SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, (Object) null);
|
||||
}
|
||||
|
||||
Object right = rightOp.getValue(state);
|
||||
if (!(right instanceof String)) {
|
||||
throw new SpelEvaluationException(rightOp.getStartPosition(),
|
||||
SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, right);
|
||||
}
|
||||
String regex = (String) right;
|
||||
|
||||
try {
|
||||
String rightString = (String) right;
|
||||
Pattern pattern = this.patternCache.get(rightString);
|
||||
Pattern pattern = this.patternCache.get(regex);
|
||||
if (pattern == null) {
|
||||
pattern = Pattern.compile(rightString);
|
||||
this.patternCache.putIfAbsent(rightString, pattern);
|
||||
checkRegexLength(regex);
|
||||
pattern = Pattern.compile(regex);
|
||||
this.patternCache.putIfAbsent(regex, pattern);
|
||||
}
|
||||
Matcher matcher = pattern.matcher(new MatcherInput(left, new AccessCount()));
|
||||
Matcher matcher = pattern.matcher(new MatcherInput(input, new AccessCount()));
|
||||
return BooleanTypedValue.forValue(matcher.matches());
|
||||
}
|
||||
catch (PatternSyntaxException ex) {
|
||||
@@ -94,6 +118,13 @@ public class OperatorMatches extends Operator {
|
||||
}
|
||||
}
|
||||
|
||||
private void checkRegexLength(String regex) {
|
||||
if (regex.length() > MAX_REGEX_LENGTH) {
|
||||
throw new SpelEvaluationException(getStartPosition(),
|
||||
SpelMessage.MAX_REGEX_LENGTH_EXCEEDED, MAX_REGEX_LENGTH);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class AccessCount {
|
||||
|
||||
@@ -111,7 +142,7 @@ public class OperatorMatches extends Operator {
|
||||
|
||||
private final CharSequence value;
|
||||
|
||||
private AccessCount access;
|
||||
private final AccessCount access;
|
||||
|
||||
public MatcherInput(CharSequence value, AccessCount access) {
|
||||
this.value = value;
|
||||
|
||||
+9
-3
@@ -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
|
||||
|
||||
+28
-4
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+13
-8
@@ -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 — 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
|
||||
|
||||
+23
-2
@@ -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,12 +21,15 @@ import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Deque;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
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;
|
||||
@@ -83,18 +86,28 @@ import org.springframework.util.StringUtils;
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Phillip Webb
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
*/
|
||||
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;
|
||||
|
||||
// For rules that build nodes, they are stacked here for return
|
||||
private final Deque<SpelNodeImpl> constructedNodes = new ArrayDeque<>();
|
||||
|
||||
// Shared cache for compiled regex patterns
|
||||
private final ConcurrentMap<String, Pattern> patternCache = new ConcurrentHashMap<>();
|
||||
|
||||
// The expression being parsed
|
||||
private String expressionString = "";
|
||||
|
||||
@@ -121,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);
|
||||
@@ -142,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)
|
||||
@@ -248,7 +269,7 @@ class InternalSpelExpressionParser extends TemplateAwareExpressionParser {
|
||||
}
|
||||
|
||||
if (tk == TokenKind.MATCHES) {
|
||||
return new OperatorMatches(t.startPos, t.endPos, expr, rhExpr);
|
||||
return new OperatorMatches(this.patternCache, t.startPos, t.endPos, expr, rhExpr);
|
||||
}
|
||||
|
||||
Assert.isTrue(tk == TokenKind.BETWEEN, "Between token expected");
|
||||
|
||||
+12
-8
@@ -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.
|
||||
@@ -48,6 +48,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Chris Beams
|
||||
* @author Sam Brannen
|
||||
* @since 3.0
|
||||
* @see StandardEvaluationContext#addMethodResolver(MethodResolver)
|
||||
*/
|
||||
@@ -225,8 +226,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
|
||||
if (targetObject instanceof Class) {
|
||||
Set<Method> result = new LinkedHashSet<>();
|
||||
// Add these so that static methods are invocable on the type: e.g. Float.valueOf(..)
|
||||
Method[] methods = getMethods(type);
|
||||
for (Method method : methods) {
|
||||
for (Method method : getMethods(type)) {
|
||||
if (Modifier.isStatic(method.getModifiers())) {
|
||||
result.add(method);
|
||||
}
|
||||
@@ -239,19 +239,23 @@ public class ReflectiveMethodResolver implements MethodResolver {
|
||||
Set<Method> result = new LinkedHashSet<>();
|
||||
// Expose interface methods (not proxy-declared overrides) for proper vararg introspection
|
||||
for (Class<?> ifc : type.getInterfaces()) {
|
||||
Method[] methods = getMethods(ifc);
|
||||
for (Method method : methods) {
|
||||
for (Method method : getMethods(ifc)) {
|
||||
if (isCandidateForInvocation(method, type)) {
|
||||
result.add(method);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Ensure methods defined in java.lang.Object are exposed for JDK proxies.
|
||||
for (Method method : getMethods(Object.class)) {
|
||||
if (isCandidateForInvocation(method, type)) {
|
||||
result.add(method);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
else {
|
||||
Set<Method> result = new LinkedHashSet<>();
|
||||
Method[] methods = getMethods(type);
|
||||
for (Method method : methods) {
|
||||
for (Method method : getMethods(type)) {
|
||||
if (isCandidateForInvocation(method, type)) {
|
||||
result.add(method);
|
||||
}
|
||||
@@ -276,7 +280,7 @@ public class ReflectiveMethodResolver implements MethodResolver {
|
||||
* Determine whether the given {@code Method} is a candidate for method resolution
|
||||
* on an instance of the given target class.
|
||||
* <p>The default implementation considers any method as a candidate, even for
|
||||
* static methods sand non-user-declared methods on the {@link Object} base class.
|
||||
* static methods and non-user-declared methods on the {@link Object} base class.
|
||||
* @param method the Method to evaluate
|
||||
* @param targetClass the concrete target class that is being introspected
|
||||
* @since 4.3.15
|
||||
|
||||
+14
-1
@@ -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);
|
||||
|
||||
+5
-3
@@ -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.
|
||||
@@ -35,7 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*/
|
||||
public abstract class AbstractExpressionTests {
|
||||
|
||||
private static final boolean DEBUG = false;
|
||||
protected static final boolean DEBUG = false;
|
||||
|
||||
protected static final boolean SHOULD_BE_WRITABLE = true;
|
||||
|
||||
@@ -202,7 +202,9 @@ public abstract class AbstractExpressionTests {
|
||||
protected void parseAndCheckError(String expression, SpelMessage expectedMessage, Object... otherProperties) {
|
||||
assertThatExceptionOfType(SpelParseException.class).isThrownBy(() -> {
|
||||
Expression expr = parser.parseExpression(expression);
|
||||
SpelUtilities.printAbstractSyntaxTree(System.out, expr);
|
||||
if (DEBUG) {
|
||||
SpelUtilities.printAbstractSyntaxTree(System.out, expr);
|
||||
}
|
||||
}).satisfies(ex -> {
|
||||
assertThat(ex.getMessageCode()).isEqualTo(expectedMessage);
|
||||
if (otherProperties != null && otherProperties.length != 0) {
|
||||
|
||||
+77
-25
@@ -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.
|
||||
@@ -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));
|
||||
@@ -127,17 +144,6 @@ class EvaluationTests extends AbstractExpressionTests {
|
||||
evaluate("null?.null?.null", null, null);
|
||||
}
|
||||
|
||||
@Test // SPR-16731
|
||||
void matchesWithPatternAccessThreshold() {
|
||||
String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$";
|
||||
String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches \'" + pattern + "\'";
|
||||
Expression expr = parser.parseExpression(expression);
|
||||
assertThatExceptionOfType(SpelEvaluationException.class)
|
||||
.isThrownBy(expr::getValue)
|
||||
.withCauseInstanceOf(IllegalStateException.class)
|
||||
.satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(SpelMessage.FLAWED_PATTERN));
|
||||
}
|
||||
|
||||
// mixing operators
|
||||
@Test
|
||||
void mixingOperators() {
|
||||
@@ -146,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
|
||||
@@ -460,28 +482,47 @@ class EvaluationTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void relOperatorsMatches01() {
|
||||
evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void relOperatorsMatches02() {
|
||||
void matchesTrue() {
|
||||
evaluate("'5.00' matches '^-?\\d+(\\.\\d{2})?$'", "true", Boolean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void relOperatorsMatches03() {
|
||||
void matchesFalse() {
|
||||
evaluate("'5.0067' matches '^-?\\d+(\\.\\d{2})?$'", "false", Boolean.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWithInputConversion() {
|
||||
evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int --> string
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchesWithNullInput() {
|
||||
evaluateAndCheckError("null matches '^.*$'", SpelMessage.INVALID_FIRST_OPERAND_FOR_MATCHES_OPERATOR, 0, null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void relOperatorsMatches04() {
|
||||
void matchesWithNullPattern() {
|
||||
evaluateAndCheckError("'abc' matches null", SpelMessage.INVALID_SECOND_OPERAND_FOR_MATCHES_OPERATOR, 14, null);
|
||||
}
|
||||
|
||||
@Test // SPR-16731
|
||||
void matchesWithPatternAccessThreshold() {
|
||||
String pattern = "^(?=[a-z0-9-]{1,47})([a-z0-9]+[-]{0,1}){1,47}[a-z0-9]{1}$";
|
||||
String expression = "'abcde-fghijklmn-o42pasdfasdfasdf.qrstuvwxyz10x.xx.yyy.zasdfasfd' matches '" + pattern + "'";
|
||||
evaluateAndCheckError(expression, SpelMessage.FLAWED_PATTERN);
|
||||
}
|
||||
|
||||
@Test
|
||||
void relOperatorsMatches05() {
|
||||
evaluate("27 matches '^.*2.*$'", true, Boolean.class); // conversion int>string
|
||||
void matchesWithPatternLengthThreshold() {
|
||||
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(1001);
|
||||
evaluateAndCheckError("'abc' matches '" + pattern + "'", Boolean.class, SpelMessage.MAX_REGEX_LENGTH_EXCEEDED);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1448,7 +1489,9 @@ class EvaluationTests extends AbstractExpressionTests {
|
||||
private void expectFail(ExpressionParser parser, EvaluationContext eContext, String expressionString, SpelMessage messageCode) {
|
||||
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() -> {
|
||||
Expression e = parser.parseExpression(expressionString);
|
||||
SpelUtilities.printAbstractSyntaxTree(System.out, e);
|
||||
if (DEBUG) {
|
||||
SpelUtilities.printAbstractSyntaxTree(System.out, e);
|
||||
}
|
||||
e.getValue(eContext);
|
||||
}).satisfies(ex -> assertThat(ex.getMessageCode()).isEqualTo(messageCode));
|
||||
}
|
||||
@@ -1456,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 {
|
||||
|
||||
|
||||
+118
-62
@@ -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.
|
||||
@@ -21,22 +21,26 @@ import java.math.BigInteger;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.expression.Expression;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Tests the evaluation of expressions using relational operators.
|
||||
* Tests the evaluation of expressions using various operators.
|
||||
*
|
||||
* @author Andy Clement
|
||||
* @author Juergen Hoeller
|
||||
* @author Giovanni Dall'Oglio Risso
|
||||
* @author Sam Brannen
|
||||
*/
|
||||
class OperatorTests extends AbstractExpressionTests {
|
||||
|
||||
@Test
|
||||
void testEqual() {
|
||||
void equal() {
|
||||
evaluate("3 == 5", false, Boolean.class);
|
||||
evaluate("5 == 3", false, Boolean.class);
|
||||
evaluate("6 == 6", true, Boolean.class);
|
||||
@@ -85,7 +89,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testNotEqual() {
|
||||
void notEqual() {
|
||||
evaluate("3 != 5", true, Boolean.class);
|
||||
evaluate("5 != 3", true, Boolean.class);
|
||||
evaluate("6 != 6", false, Boolean.class);
|
||||
@@ -134,7 +138,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLessThan() {
|
||||
void lessThan() {
|
||||
evaluate("5 < 5", false, Boolean.class);
|
||||
evaluate("3 < 5", true, Boolean.class);
|
||||
evaluate("5 < 3", false, Boolean.class);
|
||||
@@ -176,7 +180,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLessThanOrEqual() {
|
||||
void lessThanOrEqual() {
|
||||
evaluate("3 <= 5", true, Boolean.class);
|
||||
evaluate("5 <= 3", false, Boolean.class);
|
||||
evaluate("6 <= 6", true, Boolean.class);
|
||||
@@ -225,7 +229,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGreaterThan() {
|
||||
void greaterThan() {
|
||||
evaluate("3 > 5", false, Boolean.class);
|
||||
evaluate("5 > 3", true, Boolean.class);
|
||||
evaluate("3L > 5L", false, Boolean.class);
|
||||
@@ -266,7 +270,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGreaterThanOrEqual() {
|
||||
void greaterThanOrEqual() {
|
||||
evaluate("3 >= 5", false, Boolean.class);
|
||||
evaluate("5 >= 3", true, Boolean.class);
|
||||
evaluate("6 >= 6", true, Boolean.class);
|
||||
@@ -315,27 +319,22 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIntegerLiteral() {
|
||||
void integerLiteral() {
|
||||
evaluate("3", 3, Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRealLiteral() {
|
||||
void realLiteral() {
|
||||
evaluate("3.5", 3.5d, Double.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiplyStringInt() {
|
||||
evaluate("'a' * 5", "aaaaa", String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultiplyDoubleDoubleGivesDouble() {
|
||||
void multiplyDoubleDoubleGivesDouble() {
|
||||
evaluate("3.0d * 5.0d", 15.0d, Double.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedOperandsBigDecimal() {
|
||||
void mixedOperandsBigDecimal() {
|
||||
evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class);
|
||||
evaluate("3L * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class);
|
||||
evaluate("3.0d * new java.math.BigDecimal('5')", new BigDecimal("15.0"), BigDecimal.class);
|
||||
@@ -361,19 +360,19 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMathOperatorAdd02() {
|
||||
void mathOperatorAdd02() {
|
||||
evaluate("'hello' + ' ' + 'world'", "hello world", String.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMathOperatorsInChains() {
|
||||
void mathOperatorsInChains() {
|
||||
evaluate("1+2+3",6,Integer.class);
|
||||
evaluate("2*3*4",24,Integer.class);
|
||||
evaluate("12-1-2",9,Integer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIntegerArithmetic() {
|
||||
void integerArithmetic() {
|
||||
evaluate("2 + 4", "6", Integer.class);
|
||||
evaluate("5 - 4", "1", Integer.class);
|
||||
evaluate("3 * 5", 15, Integer.class);
|
||||
@@ -388,56 +387,47 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPlus() {
|
||||
void plus() {
|
||||
evaluate("7 + 2", "9", Integer.class);
|
||||
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");
|
||||
SpelExpression expr = (SpelExpression) parser.parseExpression("+3");
|
||||
assertThat(expr.toStringAST()).isEqualTo("+3");
|
||||
expr = (SpelExpression)parser.parseExpression("2+3");
|
||||
expr = (SpelExpression) parser.parseExpression("2+3");
|
||||
assertThat(expr.toStringAST()).isEqualTo("(2 + 3)");
|
||||
|
||||
// use as a unary operator
|
||||
evaluate("+5d",5d,Double.class);
|
||||
evaluate("+5L",5L,Long.class);
|
||||
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);
|
||||
evaluate("+5d", 5d, Double.class);
|
||||
evaluate("+5L", 5L, Long.class);
|
||||
evaluate("+5", 5, Integer.class);
|
||||
evaluate("+new java.math.BigDecimal('5')", new BigDecimal("5"), BigDecimal.class);
|
||||
evaluateAndCheckError("+'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMinus() {
|
||||
void minus() {
|
||||
evaluate("'c' - 2", "a", String.class);
|
||||
evaluate("3.0f - 5.0f", -2.0f, Float.class);
|
||||
evaluateAndCheckError("'ab' - 2", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
|
||||
evaluateAndCheckError("2-'ab'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
|
||||
SpelExpression expr = (SpelExpression)parser.parseExpression("-3");
|
||||
SpelExpression expr = (SpelExpression) parser.parseExpression("-3");
|
||||
assertThat(expr.toStringAST()).isEqualTo("-3");
|
||||
expr = (SpelExpression)parser.parseExpression("2-3");
|
||||
expr = (SpelExpression) parser.parseExpression("2-3");
|
||||
assertThat(expr.toStringAST()).isEqualTo("(2 - 3)");
|
||||
|
||||
evaluate("-5d",-5d,Double.class);
|
||||
evaluate("-5L",-5L,Long.class);
|
||||
evaluate("-5d", -5d, Double.class);
|
||||
evaluate("-5L", -5L, Long.class);
|
||||
evaluate("-5", -5, Integer.class);
|
||||
evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"),BigDecimal.class);
|
||||
evaluate("-new java.math.BigDecimal('5')", new BigDecimal("-5"), BigDecimal.class);
|
||||
evaluateAndCheckError("-'abc'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testModulus() {
|
||||
void modulus() {
|
||||
evaluate("3%2",1,Integer.class);
|
||||
evaluate("3L%2L",1L,Long.class);
|
||||
evaluate("3.0f%2.0f",1f,Float.class);
|
||||
@@ -448,7 +438,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDivide() {
|
||||
void divide() {
|
||||
evaluate("3.0f / 5.0f", 0.6f, Float.class);
|
||||
evaluate("4L/2L",2L,Long.class);
|
||||
evaluate("3.0f div 5.0f", 0.6f, Float.class);
|
||||
@@ -461,17 +451,17 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMathOperatorDivide_ConvertToDouble() {
|
||||
void mathOperatorDivide_ConvertToDouble() {
|
||||
evaluateAndAskForReturnType("8/4", 2.0, Double.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMathOperatorDivide04_ConvertToFloat() {
|
||||
void mathOperatorDivide04_ConvertToFloat() {
|
||||
evaluateAndAskForReturnType("8/4", 2.0F, Float.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDoubles() {
|
||||
void doubles() {
|
||||
evaluate("3.0d == 5.0d", false, Boolean.class);
|
||||
evaluate("3.0d == 3.0d", true, Boolean.class);
|
||||
evaluate("3.0d != 5.0d", true, Boolean.class);
|
||||
@@ -484,7 +474,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBigDecimals() {
|
||||
void bigDecimals() {
|
||||
evaluate("3 + new java.math.BigDecimal('5')", new BigDecimal("8"), BigDecimal.class);
|
||||
evaluate("3 - new java.math.BigDecimal('5')", new BigDecimal("-2"), BigDecimal.class);
|
||||
evaluate("3 * new java.math.BigDecimal('5')", new BigDecimal("15"), BigDecimal.class);
|
||||
@@ -495,7 +485,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOperatorNames() {
|
||||
void operatorNames() {
|
||||
Operator node = getOperatorNode((SpelExpression)parser.parseExpression("1==3"));
|
||||
assertThat(node.getOperatorName()).isEqualTo("==");
|
||||
|
||||
@@ -534,22 +524,22 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOperatorOverloading() {
|
||||
void operatorOverloading() {
|
||||
evaluateAndCheckError("'a' * '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
|
||||
evaluateAndCheckError("'a' ^ '2'", SpelMessage.OPERATOR_NOT_SUPPORTED_BETWEEN_TYPES);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPower() {
|
||||
evaluate("3^2",9,Integer.class);
|
||||
evaluate("3.0d^2.0d",9.0d,Double.class);
|
||||
evaluate("3L^2L",9L,Long.class);
|
||||
void power() {
|
||||
evaluate("3^2", 9, Integer.class);
|
||||
evaluate("3.0d^2.0d", 9.0d, Double.class);
|
||||
evaluate("3L^2L", 9L, Long.class);
|
||||
evaluate("(2^32)^2", 9223372036854775807L, Long.class);
|
||||
evaluate("new java.math.BigDecimal('5') ^ 3", new BigDecimal("125"), BigDecimal.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedOperands_FloatsAndDoubles() {
|
||||
void mixedOperands_FloatsAndDoubles() {
|
||||
evaluate("3.0d + 5.0f", 8.0d, Double.class);
|
||||
evaluate("3.0D - 5.0f", -2.0d, Double.class);
|
||||
evaluate("3.0f * 5.0d", 15.0d, Double.class);
|
||||
@@ -558,7 +548,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMixedOperands_DoublesAndInts() {
|
||||
void mixedOperands_DoublesAndInts() {
|
||||
evaluate("3.0d + 5", 8.0d, Double.class);
|
||||
evaluate("3.0D - 5", -2.0d, Double.class);
|
||||
evaluate("3.0f * 5", 15.0f, Float.class);
|
||||
@@ -569,7 +559,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testStrings() {
|
||||
void strings() {
|
||||
evaluate("'abc' == 'abc'", true, Boolean.class);
|
||||
evaluate("'abc' == 'def'", false, Boolean.class);
|
||||
evaluate("'abc' != 'abc'", false, Boolean.class);
|
||||
@@ -577,7 +567,73 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testLongs() {
|
||||
void stringRepeat() {
|
||||
evaluate("'abc' * 0", "", String.class);
|
||||
evaluate("'abc' * 1", "abc", String.class);
|
||||
evaluate("'abc' * 2", "abcabc", String.class);
|
||||
|
||||
Expression expr = parser.parseExpression("'a' * 256");
|
||||
assertThat(expr.getValue(context, String.class)).hasSize(256);
|
||||
|
||||
// 4 is the position of the '*' (repeat operator)
|
||||
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);
|
||||
evaluate("3L == 3L", true, Boolean.class);
|
||||
evaluate("3L != 4L", true, Boolean.class);
|
||||
@@ -588,7 +644,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBigIntegers() {
|
||||
void bigIntegers() {
|
||||
evaluate("3 + new java.math.BigInteger('5')", new BigInteger("8"), BigInteger.class);
|
||||
evaluate("3 - new java.math.BigInteger('5')", new BigInteger("-2"), BigInteger.class);
|
||||
evaluate("3 * new java.math.BigInteger('5')", new BigInteger("15"), BigInteger.class);
|
||||
@@ -619,7 +675,7 @@ class OperatorTests extends AbstractExpressionTests {
|
||||
}
|
||||
|
||||
|
||||
public static class BaseComparable implements Comparable<BaseComparable> {
|
||||
static class BaseComparable implements Comparable<BaseComparable> {
|
||||
|
||||
@Override
|
||||
public int compareTo(BaseComparable other) {
|
||||
|
||||
+1
-4
@@ -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.
|
||||
@@ -1817,7 +1817,6 @@ class SpelReproTests extends AbstractExpressionTests {
|
||||
static class CCC {
|
||||
|
||||
public boolean method(Object o) {
|
||||
System.out.println(o);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1889,7 +1888,6 @@ class SpelReproTests extends AbstractExpressionTests {
|
||||
static class Foo2 {
|
||||
|
||||
public void execute(String str) {
|
||||
System.out.println("Value: " + str);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1964,7 +1962,6 @@ class SpelReproTests extends AbstractExpressionTests {
|
||||
public static class ReflectionUtil<T extends Number> {
|
||||
|
||||
public Object methodToCall(T param) {
|
||||
System.out.println(param + " " + param.getClass());
|
||||
return "Object methodToCall(T param)";
|
||||
}
|
||||
|
||||
|
||||
+19
-1
@@ -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.
|
||||
@@ -19,13 +19,17 @@ package org.springframework.expression.spel.support;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.core.convert.TypeDescriptor;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.MethodExecutor;
|
||||
import org.springframework.expression.MethodResolver;
|
||||
import org.springframework.expression.ParseException;
|
||||
import org.springframework.expression.PropertyAccessor;
|
||||
import org.springframework.expression.TypedValue;
|
||||
@@ -364,6 +368,20 @@ public class ReflectionHelperTests extends AbstractExpressionTests {
|
||||
field.write(ctx, tester, "field", null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void reflectiveMethodResolverForJdkProxies() throws Exception {
|
||||
Object proxy = Proxy.newProxyInstance(getClass().getClassLoader(), new Class<?>[] { Runnable.class }, (p, m, args) -> null);
|
||||
|
||||
MethodResolver resolver = new ReflectiveMethodResolver();
|
||||
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
|
||||
|
||||
MethodExecutor bogus = resolver.resolve(evaluationContext, proxy, "bogus", Collections.emptyList());
|
||||
assertThat(bogus).as("MethodExecutor for bogus()").isNull();
|
||||
MethodExecutor toString = resolver.resolve(evaluationContext, proxy, "toString", Collections.emptyList());
|
||||
assertThat(toString).as("MethodExecutor for toString()").isNotNull();
|
||||
MethodExecutor hashCode = resolver.resolve(evaluationContext, proxy, "hashCode", Collections.emptyList());
|
||||
assertThat(hashCode).as("MethodExecutor for hashCode()").isNotNull();
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to validate the match returned from a compareArguments call.
|
||||
|
||||
+74
-50
@@ -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.
|
||||
@@ -48,31 +48,42 @@ import org.springframework.util.StringUtils;
|
||||
/**
|
||||
* {@link RowMapper} implementation that converts a row into a new instance
|
||||
* of the specified mapped target class. The mapped target class must be a
|
||||
* top-level class and it must have a default or no-arg constructor.
|
||||
* top-level class or {@code static} nested class, and it must have a default or
|
||||
* no-arg constructor.
|
||||
*
|
||||
* <p>Column values are mapped based on matching the column name as obtained from result set
|
||||
* meta-data to public setters for the corresponding properties. The names are matched either
|
||||
* directly or by transforming a name separating the parts with underscores to the same name
|
||||
* using "camel" case.
|
||||
* <p>Column values are mapped based on matching the column name (as obtained from
|
||||
* result set meta-data) to public setters in the target class for the corresponding
|
||||
* properties. The names are matched either directly or by transforming a name
|
||||
* separating the parts with underscores to the same name using "camel" case.
|
||||
*
|
||||
* <p>Mapping is provided for fields in the target class for many common types, e.g.:
|
||||
* String, boolean, Boolean, byte, Byte, short, Short, int, Integer, long, Long,
|
||||
* float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
|
||||
* <p>Mapping is provided for properties in the target class for many common types —
|
||||
* for example: String, boolean, Boolean, byte, Byte, short, Short, int, Integer,
|
||||
* long, Long, float, Float, double, Double, BigDecimal, {@code java.util.Date}, etc.
|
||||
*
|
||||
* <p>To facilitate mapping between columns and fields that don't have matching names,
|
||||
* try using column aliases in the SQL statement like "select fname as first_name from customer".
|
||||
* <p>To facilitate mapping between columns and properties that don't have matching
|
||||
* names, try using column aliases in the SQL statement like
|
||||
* {@code "select fname as first_name from customer"}, where {@code first_name}
|
||||
* can be mapped to a {@code setFirstName(String)} method in the target class.
|
||||
*
|
||||
* <p>For 'null' values read from the database, we will attempt to call the setter, but in the case of
|
||||
* Java primitives, this causes a TypeMismatchException. This class can be configured (using the
|
||||
* primitivesDefaultedForNullValue property) to trap this exception and use the primitives default value.
|
||||
* Be aware that if you use the values from the generated bean to update the database the primitive value
|
||||
* will have been set to the primitive's default value instead of null.
|
||||
* <p>For a {@code NULL} value read from the database, an attempt will be made to
|
||||
* call the corresponding setter method with {@code null}, but in the case of
|
||||
* Java primitives this will result in a {@link TypeMismatchException} by default.
|
||||
* To ignore {@code NULL} database values for all primitive properties in the
|
||||
* target class, set the {@code primitivesDefaultedForNullValue} flag to
|
||||
* {@code true}. See {@link #setPrimitivesDefaultedForNullValue(boolean)} for
|
||||
* details.
|
||||
*
|
||||
* <p>Please note that this class is designed to provide convenience rather than high performance.
|
||||
* For best performance, consider using a custom {@link RowMapper} implementation.
|
||||
* <p>If you need to map to a target class which has a <em>data class</em> constructor
|
||||
* — for example, a Java {@code record} or a Kotlin {@code data} class —
|
||||
* use {@link DataClassRowMapper} instead.
|
||||
*
|
||||
* <p>Please note that this class is designed to provide convenience rather than
|
||||
* high performance. For best performance, consider using a custom {@code RowMapper}
|
||||
* implementation.
|
||||
*
|
||||
* @author Thomas Risberg
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 2.5
|
||||
* @param <T> the result type
|
||||
* @see DataClassRowMapper
|
||||
@@ -89,20 +100,24 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
/** Whether we're strictly validating. */
|
||||
private boolean checkFullyPopulated = false;
|
||||
|
||||
/** Whether we're defaulting primitives when mapping a null value. */
|
||||
/**
|
||||
* Whether {@code NULL} database values should be ignored for primitive
|
||||
* properties in the target class.
|
||||
* @see #setPrimitivesDefaultedForNullValue(boolean)
|
||||
*/
|
||||
private boolean primitivesDefaultedForNullValue = false;
|
||||
|
||||
/** ConversionService for binding JDBC values to bean properties. */
|
||||
@Nullable
|
||||
private ConversionService conversionService = DefaultConversionService.getSharedInstance();
|
||||
|
||||
/** Map of the fields we provide mapping for. */
|
||||
/** Map of the properties we provide mapping for. */
|
||||
@Nullable
|
||||
private Map<String, PropertyDescriptor> mappedFields;
|
||||
private Map<String, PropertyDescriptor> mappedProperties;
|
||||
|
||||
/** Set of bean properties we provide mapping for. */
|
||||
/** Set of bean property names we provide mapping for. */
|
||||
@Nullable
|
||||
private Set<String> mappedProperties;
|
||||
private Set<String> mappedPropertyNames;
|
||||
|
||||
|
||||
/**
|
||||
@@ -126,7 +141,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
* Create a new {@code BeanPropertyRowMapper}.
|
||||
* @param mappedClass the class that each row should be mapped to
|
||||
* @param checkFullyPopulated whether we're strictly validating that
|
||||
* all bean properties have been mapped from corresponding database fields
|
||||
* all bean properties have been mapped from corresponding database columns
|
||||
*/
|
||||
public BeanPropertyRowMapper(Class<T> mappedClass, boolean checkFullyPopulated) {
|
||||
initialize(mappedClass);
|
||||
@@ -159,7 +174,7 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
/**
|
||||
* Set whether we're strictly validating that all bean properties have been mapped
|
||||
* from corresponding database fields.
|
||||
* from corresponding database columns.
|
||||
* <p>Default is {@code false}, accepting unpopulated properties in the target bean.
|
||||
*/
|
||||
public void setCheckFullyPopulated(boolean checkFullyPopulated) {
|
||||
@@ -168,24 +183,33 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
/**
|
||||
* Return whether we're strictly validating that all bean properties have been
|
||||
* mapped from corresponding database fields.
|
||||
* mapped from corresponding database columns.
|
||||
*/
|
||||
public boolean isCheckFullyPopulated() {
|
||||
return this.checkFullyPopulated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set whether we're defaulting Java primitives in the case of mapping a null value
|
||||
* from corresponding database fields.
|
||||
* <p>Default is {@code false}, throwing an exception when nulls are mapped to Java primitives.
|
||||
* Set whether a {@code NULL} database column value should be ignored when
|
||||
* mapping to a corresponding primitive property in the target class.
|
||||
* <p>Default is {@code false}, throwing an exception when nulls are mapped
|
||||
* to Java primitives.
|
||||
* <p>If this flag is set to {@code true} and you use an <em>ignored</em>
|
||||
* primitive property value from the mapped bean to update the database, the
|
||||
* value in the database will be changed from {@code NULL} to the current value
|
||||
* of that primitive property. That value may be the property's initial value
|
||||
* (potentially Java's default value for the respective primitive type), or
|
||||
* it may be some other value set for the property in the default constructor
|
||||
* (or initialization block) or as a side effect of setting some other property
|
||||
* in the mapped bean.
|
||||
*/
|
||||
public void setPrimitivesDefaultedForNullValue(boolean primitivesDefaultedForNullValue) {
|
||||
this.primitivesDefaultedForNullValue = primitivesDefaultedForNullValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether we're defaulting Java primitives in the case of mapping a null value
|
||||
* from corresponding database fields.
|
||||
* Get the value of the {@code primitivesDefaultedForNullValue} flag.
|
||||
* @see #setPrimitivesDefaultedForNullValue(boolean)
|
||||
*/
|
||||
public boolean isPrimitivesDefaultedForNullValue() {
|
||||
return this.primitivesDefaultedForNullValue;
|
||||
@@ -220,31 +244,31 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
*/
|
||||
protected void initialize(Class<T> mappedClass) {
|
||||
this.mappedClass = mappedClass;
|
||||
this.mappedFields = new HashMap<>();
|
||||
this.mappedProperties = new HashSet<>();
|
||||
this.mappedProperties = new HashMap<>();
|
||||
this.mappedPropertyNames = new HashSet<>();
|
||||
|
||||
for (PropertyDescriptor pd : BeanUtils.getPropertyDescriptors(mappedClass)) {
|
||||
if (pd.getWriteMethod() != null) {
|
||||
String lowerCaseName = lowerCaseName(pd.getName());
|
||||
this.mappedFields.put(lowerCaseName, pd);
|
||||
this.mappedProperties.put(lowerCaseName, pd);
|
||||
String underscoreName = underscoreName(pd.getName());
|
||||
if (!lowerCaseName.equals(underscoreName)) {
|
||||
this.mappedFields.put(underscoreName, pd);
|
||||
this.mappedProperties.put(underscoreName, pd);
|
||||
}
|
||||
this.mappedProperties.add(pd.getName());
|
||||
this.mappedPropertyNames.add(pd.getName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified property from the mapped fields.
|
||||
* Remove the specified property from the mapped properties.
|
||||
* @param propertyName the property name (as used by property descriptors)
|
||||
* @since 5.3.9
|
||||
*/
|
||||
protected void suppressProperty(String propertyName) {
|
||||
if (this.mappedFields != null) {
|
||||
this.mappedFields.remove(lowerCaseName(propertyName));
|
||||
this.mappedFields.remove(underscoreName(propertyName));
|
||||
if (this.mappedProperties != null) {
|
||||
this.mappedProperties.remove(lowerCaseName(propertyName));
|
||||
this.mappedProperties.remove(underscoreName(propertyName));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +330,8 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
|
||||
for (int index = 1; index <= columnCount; index++) {
|
||||
String column = JdbcUtils.lookupColumnName(rsmd, index);
|
||||
String field = lowerCaseName(StringUtils.delete(column, " "));
|
||||
PropertyDescriptor pd = (this.mappedFields != null ? this.mappedFields.get(field) : null);
|
||||
String property = lowerCaseName(StringUtils.delete(column, " "));
|
||||
PropertyDescriptor pd = (this.mappedProperties != null ? this.mappedProperties.get(property) : null);
|
||||
if (pd != null) {
|
||||
try {
|
||||
Object value = getColumnValue(rs, index, pd);
|
||||
@@ -321,11 +345,11 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
catch (TypeMismatchException ex) {
|
||||
if (value == null && this.primitivesDefaultedForNullValue) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Intercepted TypeMismatchException for row " + rowNumber +
|
||||
" and column '" + column + "' with null value when setting property '" +
|
||||
pd.getName() + "' of type '" +
|
||||
ClassUtils.getQualifiedName(pd.getPropertyType()) +
|
||||
"' on object: " + mappedObject, ex);
|
||||
String propertyType = ClassUtils.getQualifiedName(pd.getPropertyType());
|
||||
logger.debug(String.format(
|
||||
"Ignoring intercepted TypeMismatchException for row %d and column '%s' " +
|
||||
"with null value when setting property '%s' of type '%s' on object: %s",
|
||||
rowNumber, column, pd.getName(), propertyType, mappedObject), ex);
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -343,9 +367,9 @@ public class BeanPropertyRowMapper<T> implements RowMapper<T> {
|
||||
}
|
||||
}
|
||||
|
||||
if (populatedProperties != null && !populatedProperties.equals(this.mappedProperties)) {
|
||||
throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all fields " +
|
||||
"necessary to populate object of " + this.mappedClass + ": " + this.mappedProperties);
|
||||
if (populatedProperties != null && !populatedProperties.equals(this.mappedPropertyNames)) {
|
||||
throw new InvalidDataAccessApiUsageException("Given ResultSet does not contain all properties " +
|
||||
"necessary to populate object of " + this.mappedClass + ": " + this.mappedPropertyNames);
|
||||
}
|
||||
|
||||
return mappedObject;
|
||||
|
||||
@@ -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.
|
||||
@@ -31,15 +31,30 @@ import org.springframework.util.Assert;
|
||||
/**
|
||||
* {@link RowMapper} implementation that converts a row into a new instance
|
||||
* of the specified mapped target class. The mapped target class must be a
|
||||
* top-level class and may either expose a data class constructor with named
|
||||
* parameters corresponding to column names or classic bean property setters
|
||||
* (or even a combination of both).
|
||||
* top-level class or {@code static} nested class, and it may expose either a
|
||||
* <em>data class</em> constructor with named parameters corresponding to column
|
||||
* names or classic bean property setter methods with property names corresponding
|
||||
* to column names (or even a combination of both).
|
||||
*
|
||||
* <p>The term "data class" applies to Java <em>records</em>, Kotlin <em>data
|
||||
* classes</em>, and any class which has a constructor with named parameters
|
||||
* that are intended to be mapped to corresponding column names.
|
||||
*
|
||||
* <p>When combining a data class constructor with setter methods, any property
|
||||
* mapped successfully via a constructor argument will not be mapped additionally
|
||||
* via a corresponding setter method. This means that constructor arguments take
|
||||
* precedence over property setter methods.
|
||||
*
|
||||
* <p>Note that this class extends {@link BeanPropertyRowMapper} and can
|
||||
* therefore serve as a common choice for any mapped target class, flexibly
|
||||
* adapting to constructor style versus setter methods in the mapped class.
|
||||
*
|
||||
* <p>Please note that this class is designed to provide convenience rather than
|
||||
* high performance. For best performance, consider using a custom {@code RowMapper}
|
||||
* implementation.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 5.3
|
||||
* @param <T> the result type
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
@@ -24,21 +24,21 @@ import org.springframework.lang.Nullable;
|
||||
/**
|
||||
* An interface used by {@link JdbcTemplate} for mapping rows of a
|
||||
* {@link java.sql.ResultSet} on a per-row basis. Implementations of this
|
||||
* interface perform the actual work of mapping each row to a result object,
|
||||
* interface perform the actual work of mapping each row to a result object
|
||||
* but don't need to worry about exception handling.
|
||||
* {@link java.sql.SQLException SQLExceptions} will be caught and handled
|
||||
* by the calling JdbcTemplate.
|
||||
* by the calling {@code JdbcTemplate}.
|
||||
*
|
||||
* <p>Typically used either for {@link JdbcTemplate}'s query methods
|
||||
* or for out parameters of stored procedures. RowMapper objects are
|
||||
* <p>Typically used either for {@code JdbcTemplate}'s query methods or for
|
||||
* {@code out} parameters of stored procedures. {@code RowMapper} objects are
|
||||
* typically stateless and thus reusable; they are an ideal choice for
|
||||
* implementing row-mapping logic in a single place.
|
||||
*
|
||||
* <p>Alternatively, consider subclassing
|
||||
* {@link org.springframework.jdbc.object.MappingSqlQuery} from the
|
||||
* {@code jdbc.object} package: Instead of working with separate
|
||||
* JdbcTemplate and RowMapper objects, you can build executable query
|
||||
* objects (containing row-mapping logic) in that style.
|
||||
* {@code jdbc.object} package: instead of working with separate
|
||||
* {@code JdbcTemplate} and {@code RowMapper} objects, you can build executable
|
||||
* query objects (containing row-mapping logic) in that style.
|
||||
*
|
||||
* @author Thomas Risberg
|
||||
* @author Juergen Hoeller
|
||||
@@ -52,13 +52,13 @@ import org.springframework.lang.Nullable;
|
||||
public interface RowMapper<T> {
|
||||
|
||||
/**
|
||||
* Implementations must implement this method to map each row of data
|
||||
* in the ResultSet. This method should not call {@code next()} on
|
||||
* the ResultSet; it is only supposed to map values of the current row.
|
||||
* @param rs the ResultSet to map (pre-initialized for the current row)
|
||||
* Implementations must implement this method to map each row of data in the
|
||||
* {@code ResultSet}. This method should not call {@code next()} on the
|
||||
* {@code ResultSet}; it is only supposed to map values of the current row.
|
||||
* @param rs the {@code ResultSet} to map (pre-initialized for the current row)
|
||||
* @param rowNum the number of the current row
|
||||
* @return the result object for the current row (may be {@code null})
|
||||
* @throws SQLException if an SQLException is encountered getting
|
||||
* @throws SQLException if an SQLException is encountered while getting
|
||||
* column values (that is, there's no need to catch SQLException)
|
||||
*/
|
||||
@Nullable
|
||||
|
||||
+37
-34
@@ -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,8 +16,6 @@
|
||||
|
||||
package org.springframework.jdbc.core;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.CsvSource;
|
||||
@@ -44,12 +42,15 @@ import static org.assertj.core.api.Assertions.assertThatNoException;
|
||||
*/
|
||||
class BeanPropertyRowMapperTests extends AbstractRowMapperTests {
|
||||
|
||||
private static final String SELECT_NULL_AS_AGE = "select null as age from people";
|
||||
|
||||
|
||||
@Test
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
void overridingDifferentClassDefinedForMapping() {
|
||||
BeanPropertyRowMapper mapper = new BeanPropertyRowMapper(Person.class);
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
|
||||
mapper.setMappedClass(Long.class));
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> mapper.setMappedClass(Long.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -61,104 +62,106 @@ class BeanPropertyRowMapperTests extends AbstractRowMapperTests {
|
||||
@Test
|
||||
void staticQueryWithRowMapper() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<Person> result = mock.getJdbcTemplate().query(
|
||||
Person person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(Person.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingWithInheritance() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<ConcretePerson> result = mock.getJdbcTemplate().query(
|
||||
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(ConcretePerson.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingWithNoUnpopulatedFieldsFound() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<ConcretePerson> result = mock.getJdbcTemplate().query(
|
||||
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(ConcretePerson.class, true));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingWithUnpopulatedFieldsNotChecked() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<ExtendedPerson> result = mock.getJdbcTemplate().query(
|
||||
ExtendedPerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(ExtendedPerson.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingWithUnpopulatedFieldsNotAccepted() throws Exception {
|
||||
BeanPropertyRowMapper<ExtendedPerson> mapper = new BeanPropertyRowMapper<>(ExtendedPerson.class, true);
|
||||
Mock mock = new Mock();
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class).isThrownBy(() ->
|
||||
mock.getJdbcTemplate().query("select name, age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(ExtendedPerson.class, true)));
|
||||
assertThatExceptionOfType(InvalidDataAccessApiUsageException.class)
|
||||
.isThrownBy(() -> mock.getJdbcTemplate().query("select name, age, birth_date, balance from people", mapper));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingNullValue() throws Exception {
|
||||
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
|
||||
Mock mock = new Mock(MockType.TWO);
|
||||
assertThatExceptionOfType(TypeMismatchException.class).isThrownBy(() ->
|
||||
mock.getJdbcTemplate().query("select name, null as age, birth_date, balance from people", mapper));
|
||||
assertThatExceptionOfType(TypeMismatchException.class)
|
||||
.isThrownBy(() -> mock.getJdbcTemplate().query(SELECT_NULL_AS_AGE, mapper));
|
||||
}
|
||||
|
||||
@Test
|
||||
void mappingNullValueWithPrimitivesDefaultedForNullValue() throws Exception {
|
||||
BeanPropertyRowMapper<Person> mapper = new BeanPropertyRowMapper<>(Person.class);
|
||||
mapper.setPrimitivesDefaultedForNullValue(true);
|
||||
Mock mock = new Mock(MockType.TWO);
|
||||
Person person = mock.getJdbcTemplate().queryForObject(SELECT_NULL_AS_AGE, mapper);
|
||||
assertThat(person).extracting(Person::getAge).isEqualTo(42L);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryWithSpaceInColumnNameAndLocalDateTime() throws Exception {
|
||||
Mock mock = new Mock(MockType.THREE);
|
||||
List<SpacePerson> result = mock.getJdbcTemplate().query(
|
||||
SpacePerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select last_name as \"Last Name\", age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(SpacePerson.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryWithSpaceInColumnNameAndLocalDate() throws Exception {
|
||||
Mock mock = new Mock(MockType.THREE);
|
||||
List<DatePerson> result = mock.getJdbcTemplate().query(
|
||||
DatePerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select last_name as \"Last Name\", age, birth_date, balance from people",
|
||||
new BeanPropertyRowMapper<>(DatePerson.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryWithDirectNameMatchOnBirthDate() throws Exception {
|
||||
Mock mock = new Mock(MockType.FOUR);
|
||||
List<ConcretePerson> result = mock.getJdbcTemplate().query(
|
||||
ConcretePerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birthdate, balance from people",
|
||||
new BeanPropertyRowMapper<>(ConcretePerson.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryWithUnderscoreInColumnNameAndPersonWithMultipleAdjacentUppercaseLettersInPropertyName() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<EmailPerson> result = mock.getJdbcTemplate().query(
|
||||
EmailPerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance, e_mail from people",
|
||||
new BeanPropertyRowMapper<>(EmailPerson.class));
|
||||
assertThat(result).hasSize(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
|
||||
+15
-19
@@ -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,9 +17,7 @@
|
||||
package org.springframework.jdbc.core;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -30,50 +28,48 @@ import org.springframework.jdbc.core.test.ConstructorPersonWithSetters;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for {@link DataClassRowMapper}.
|
||||
*
|
||||
* @author Juergen Hoeller
|
||||
* @author Sam Brannen
|
||||
* @since 5.3
|
||||
*/
|
||||
public class DataClassRowMapperTests extends AbstractRowMapperTests {
|
||||
class DataClassRowMapperTests extends AbstractRowMapperTests {
|
||||
|
||||
@Test
|
||||
public void testStaticQueryWithDataClass() throws Exception {
|
||||
void staticQueryWithDataClass() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<ConstructorPerson> result = mock.getJdbcTemplate().query(
|
||||
ConstructorPerson person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance from people",
|
||||
new DataClassRowMapper<>(ConstructorPerson.class));
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
verifyPerson(result.get(0));
|
||||
verifyPerson(person);
|
||||
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticQueryWithDataClassAndGenerics() throws Exception {
|
||||
void staticQueryWithDataClassAndGenerics() throws Exception {
|
||||
Mock mock = new Mock();
|
||||
List<ConstructorPersonWithGenerics> result = mock.getJdbcTemplate().query(
|
||||
ConstructorPersonWithGenerics person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birth_date, balance from people",
|
||||
new DataClassRowMapper<>(ConstructorPersonWithGenerics.class));
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
ConstructorPersonWithGenerics person = result.get(0);
|
||||
assertThat(person.name()).isEqualTo("Bubba");
|
||||
assertThat(person.age()).isEqualTo(22L);
|
||||
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L));
|
||||
assertThat(person.balance()).isEqualTo(Collections.singletonList(new BigDecimal("1234.56")));
|
||||
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
|
||||
assertThat(person.balance()).containsExactly(new BigDecimal("1234.56"));
|
||||
|
||||
mock.verifyClosed();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticQueryWithDataClassAndSetters() throws Exception {
|
||||
void staticQueryWithDataClassAndSetters() throws Exception {
|
||||
Mock mock = new Mock(MockType.FOUR);
|
||||
List<ConstructorPersonWithSetters> result = mock.getJdbcTemplate().query(
|
||||
ConstructorPersonWithSetters person = mock.getJdbcTemplate().queryForObject(
|
||||
"select name, age, birthdate, balance from people",
|
||||
new DataClassRowMapper<>(ConstructorPersonWithSetters.class));
|
||||
assertThat(result.size()).isEqualTo(1);
|
||||
ConstructorPersonWithSetters person = result.get(0);
|
||||
assertThat(person.name()).isEqualTo("BUBBA");
|
||||
assertThat(person.age()).isEqualTo(22L);
|
||||
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new java.util.Date(1221222L));
|
||||
assertThat(person.birthDate()).usingComparator(Date::compareTo).isEqualTo(new Date(1221222L));
|
||||
assertThat(person.balance()).isEqualTo(new BigDecimal("1234.56"));
|
||||
|
||||
mock.verifyClosed();
|
||||
|
||||
@@ -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.
|
||||
@@ -31,6 +31,9 @@ public class Person {
|
||||
|
||||
private BigDecimal balance;
|
||||
|
||||
public Person() {
|
||||
this.age = 42; // custom "default" value for a primitive
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
|
||||
@@ -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.
|
||||
@@ -76,6 +76,7 @@ import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
* <em>composed annotations</em> with attribute overrides.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 4.1
|
||||
* @see EnableJms
|
||||
* @see JmsListenerAnnotationBeanPostProcessor
|
||||
@@ -110,6 +111,12 @@ public @interface JmsListener {
|
||||
|
||||
/**
|
||||
* The name for the durable subscription, if any.
|
||||
* <p>As of Spring Framework 5.3.26, if an explicit subscription name is not
|
||||
* specified, a default subscription name will be generated based on the fully
|
||||
* qualified name of the annotated listener method — for example,
|
||||
* {@code "org.example.jms.ProductListener.processRequest"} for a
|
||||
* {@code processRequest(...)} listener method in the
|
||||
* {@code org.example.jms.ProductListener} class.
|
||||
*/
|
||||
String subscription() default "";
|
||||
|
||||
|
||||
+21
-3
@@ -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.
|
||||
@@ -347,8 +347,26 @@ public class SingleConnectionFactory implements ConnectionFactory, QueueConnecti
|
||||
if (this.connection != null) {
|
||||
closeConnection(this.connection);
|
||||
}
|
||||
this.connection = doCreateConnection();
|
||||
prepareConnection(this.connection);
|
||||
// Create new (method local) connection, which is later assigned to instance connection
|
||||
// - prevention to hold instance connection without exception listener, in case when
|
||||
// some subsequent methods (after creation of connection) throws JMSException
|
||||
Connection con = doCreateConnection();
|
||||
try {
|
||||
prepareConnection(con);
|
||||
this.connection = con;
|
||||
}
|
||||
catch (JMSException ex) {
|
||||
// Attempt to close new (not used) connection to release possible resources
|
||||
try {
|
||||
con.close();
|
||||
}
|
||||
catch(Throwable th) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Could not close newly obtained JMS Connection that failed to prepare", th);
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
if (this.startedCount > 0) {
|
||||
this.connection.start();
|
||||
}
|
||||
|
||||
+46
-15
@@ -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.
|
||||
@@ -20,6 +20,7 @@ import javax.jms.JMSException;
|
||||
import javax.jms.Session;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.jms.listener.SubscriptionNameProvider;
|
||||
import org.springframework.jms.support.JmsHeaderMapper;
|
||||
import org.springframework.jms.support.converter.MessageConversionException;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -34,7 +35,7 @@ import org.springframework.util.Assert;
|
||||
* A {@link javax.jms.MessageListener} adapter that invokes a configurable
|
||||
* {@link InvocableHandlerMethod}.
|
||||
*
|
||||
* <p>Wraps the incoming {@link javax.jms.Message} to Spring's {@link Message}
|
||||
* <p>Wraps the incoming {@link javax.jms.Message} in Spring's {@link Message}
|
||||
* abstraction, copying the JMS standard headers using a configurable
|
||||
* {@link JmsHeaderMapper}.
|
||||
*
|
||||
@@ -42,13 +43,19 @@ import org.springframework.util.Assert;
|
||||
* are provided as additional arguments so that these can be injected as
|
||||
* method arguments if necessary.
|
||||
*
|
||||
* <p>As of Spring Framework 5.3.26, {@code MessagingMessageListenerAdapter} implements
|
||||
* {@link SubscriptionNameProvider} in order to provide a meaningful default
|
||||
* subscription name. See {@link #getSubscriptionName()} for details.
|
||||
*
|
||||
* @author Stephane Nicoll
|
||||
* @author Sam Brannen
|
||||
* @since 4.1
|
||||
* @see Message
|
||||
* @see JmsHeaderMapper
|
||||
* @see InvocableHandlerMethod
|
||||
*/
|
||||
public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener {
|
||||
public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener
|
||||
implements SubscriptionNameProvider {
|
||||
|
||||
@Nullable
|
||||
private InvocableHandlerMethod handlerMethod;
|
||||
@@ -83,17 +90,6 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object preProcessResponse(Object result) {
|
||||
MethodParameter returnType = getHandlerMethod().getReturnType();
|
||||
if (result instanceof Message) {
|
||||
return MessageBuilder.fromMessage((Message<?>) result)
|
||||
.setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build();
|
||||
}
|
||||
return MessageBuilder.withPayload(result).setHeader(
|
||||
AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType).build();
|
||||
}
|
||||
|
||||
protected Message<?> toMessagingMessage(javax.jms.Message jmsMessage) {
|
||||
try {
|
||||
return (Message<?>) getMessagingMessageConverter().fromMessage(jmsMessage);
|
||||
@@ -104,7 +100,7 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke the handler, wrapping any exception to a {@link ListenerExecutionFailedException}
|
||||
* Invoke the handler, wrapping any exception in a {@link ListenerExecutionFailedException}
|
||||
* with a dedicated error message.
|
||||
*/
|
||||
@Nullable
|
||||
@@ -132,4 +128,39 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageLis
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object preProcessResponse(Object result) {
|
||||
MethodParameter returnType = getHandlerMethod().getReturnType();
|
||||
MessageBuilder<?> messageBuilder = (result instanceof Message ?
|
||||
MessageBuilder.fromMessage((Message<?>) result) :
|
||||
MessageBuilder.withPayload(result));
|
||||
return messageBuilder
|
||||
.setHeader(AbstractMessageSendingTemplate.CONVERSION_HINT_HEADER, returnType)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a subscription name for this {@code MessageListener} adapter based
|
||||
* on the following rules.
|
||||
* <ul>
|
||||
* <li>If the {@link #setHandlerMethod(InvocableHandlerMethod) handlerMethod}
|
||||
* has been set, the generated subscription name takes the form of
|
||||
* {@code handlerMethod.getBeanType().getName() + "." + handlerMethod.getMethod().getName()}.</li>
|
||||
* <li>Otherwise, the generated subscription name is the result of invoking
|
||||
* {@code getClass().getName()}, which aligns with the default behavior of
|
||||
* {@link org.springframework.jms.listener.AbstractMessageListenerContainer}.</li>
|
||||
* </ul>
|
||||
* @since 5.3.26
|
||||
* @see SubscriptionNameProvider#getSubscriptionName()
|
||||
*/
|
||||
@Override
|
||||
public String getSubscriptionName() {
|
||||
if (this.handlerMethod != null) {
|
||||
return this.handlerMethod.getBeanType().getName() + "." + this.handlerMethod.getMethod().getName();
|
||||
}
|
||||
else {
|
||||
return getClass().getName();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+77
-1
@@ -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.
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.jms.connection;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import javax.jms.Connection;
|
||||
import javax.jms.ConnectionFactory;
|
||||
import javax.jms.ExceptionListener;
|
||||
@@ -30,7 +33,10 @@ import javax.jms.TopicSession;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.times;
|
||||
@@ -342,6 +348,76 @@ public class SingleConnectionFactoryTests {
|
||||
assertThat(listener.getCount()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithConnectionFactoryAndExceptionListenerAndReconnectOnExceptionWithJMSException() throws Exception {
|
||||
// Throws JMSException on setExceptionListener() method, but only at the first time
|
||||
class FailingTestConnection extends TestConnection {
|
||||
private int setExceptionListenerInvocationCounter;
|
||||
|
||||
@Override
|
||||
public void setExceptionListener(ExceptionListener exceptionListener) throws JMSException {
|
||||
setExceptionListenerInvocationCounter++;
|
||||
// Throw JMSException on first invocation
|
||||
if (setExceptionListenerInvocationCounter == 1) {
|
||||
throw new JMSException("Test JMSException (setExceptionListener())");
|
||||
}
|
||||
super.setExceptionListener(exceptionListener);
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare base JMS ConnectionFactory
|
||||
// - createConnection(1st) -> TestConnection,
|
||||
// - createConnection(2nd and next) -> FailingTestConnection
|
||||
TestConnection testCon = new TestConnection();
|
||||
FailingTestConnection failingCon = new FailingTestConnection();
|
||||
AtomicInteger createConnectionMethodCounter = new AtomicInteger();
|
||||
ConnectionFactory cf = mock(ConnectionFactory.class);
|
||||
given(cf.createConnection()).willAnswer(invocation -> {
|
||||
int methodInvocationCounter = createConnectionMethodCounter.incrementAndGet();
|
||||
return methodInvocationCounter == 1 ? testCon : failingCon;
|
||||
});
|
||||
|
||||
// Prepare SingleConnectionFactory (setReconnectOnException())
|
||||
// - internal connection exception listener should be registered
|
||||
SingleConnectionFactory scf = new SingleConnectionFactory(cf);
|
||||
scf.setReconnectOnException(true);
|
||||
Field conField = ReflectionUtils.findField(SingleConnectionFactory.class, "connection");
|
||||
conField.setAccessible(true);
|
||||
|
||||
// Get connection (1st)
|
||||
Connection con1 = scf.getConnection();
|
||||
assertThat(createConnectionMethodCounter.get()).isEqualTo(1);
|
||||
assertThat(con1).isNotNull();
|
||||
assertThat(con1.getExceptionListener()).isNotNull();
|
||||
assertThat(con1).isSameAs(testCon);
|
||||
// Get connection again, the same should be returned (shared connection till some problem)
|
||||
Connection con2 = scf.getConnection();
|
||||
assertThat(createConnectionMethodCounter.get()).isEqualTo(1);
|
||||
assertThat(con2.getExceptionListener()).isNotNull();
|
||||
assertThat(con2).isSameAs(con1);
|
||||
|
||||
// Invoke reset connection to simulate problem with connection
|
||||
// - SCF exception listener should be invoked -> connection should be set to null
|
||||
// - next attempt to invoke getConnection() must create new connection
|
||||
scf.resetConnection();
|
||||
assertThat(conField.get(scf)).isNull();
|
||||
|
||||
// Attempt to get connection again
|
||||
// - JMSException should be returned from FailingTestConnection
|
||||
// - connection should be still null (no new connection without exception listener like before fix)
|
||||
assertThatExceptionOfType(JMSException.class).isThrownBy(() -> scf.getConnection());
|
||||
assertThat(createConnectionMethodCounter.get()).isEqualTo(2);
|
||||
assertThat(conField.get(scf)).isNull();
|
||||
|
||||
// Attempt to get connection again -> FailingTestConnection should be returned
|
||||
// - no JMSException is thrown, exception listener should be present
|
||||
Connection con3 = scf.getConnection();
|
||||
assertThat(createConnectionMethodCounter.get()).isEqualTo(3);
|
||||
assertThat(con3).isNotNull();
|
||||
assertThat(con3).isSameAs(failingCon);
|
||||
assertThat(con3.getExceptionListener()).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithConnectionFactoryAndLocalExceptionListenerWithCleanup() throws JMSException {
|
||||
ConnectionFactory cf = mock(ConnectionFactory.class);
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* 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.jms.listener.adapter;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import org.springframework.jms.listener.SimpleMessageListenerContainer;
|
||||
import org.springframework.messaging.handler.invocation.InvocableHandlerMethod;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Named.named;
|
||||
import static org.junit.jupiter.params.provider.Arguments.arguments;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link MessagingMessageListenerAdapter}.
|
||||
*
|
||||
* <p>These tests are similar to those in {@link MessagingMessageListenerAdapterTests},
|
||||
* except that these tests have a different scope and do not use mocks.
|
||||
*
|
||||
* @author Sam Brannen
|
||||
* @since 5.3.26
|
||||
* @see MessagingMessageListenerAdapterTests
|
||||
*/
|
||||
class MessagingMessageListenerAdapterIntegrationTests {
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("subscriptionNames")
|
||||
void defaultSubscriptionName(Method method, String subscriptionName) {
|
||||
MessagingMessageListenerAdapter messageListenerAdaptor = new MessagingMessageListenerAdapter();
|
||||
InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(new CustomListener(), method);
|
||||
messageListenerAdaptor.setHandlerMethod(handlerMethod);
|
||||
|
||||
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer();
|
||||
assertThat(listenerContainer.getSubscriptionName()).isNull();
|
||||
|
||||
listenerContainer.setMessageListener(messageListenerAdaptor);
|
||||
assertThat(listenerContainer.getSubscriptionName()).isEqualTo(subscriptionName);
|
||||
}
|
||||
|
||||
|
||||
private static Stream<Arguments> subscriptionNames() {
|
||||
String method1 = "toUpperCase";
|
||||
String method2 = "toUpperCase(java.lang.String)";
|
||||
String method3 = "toUpperCase(java.lang.String,int)";
|
||||
String method4 = "toUpperCase(java.lang.String[])";
|
||||
String expectedName = CustomListener.class.getName() + ".toUpperCase";
|
||||
return Stream.of(
|
||||
arguments(named(method1, findMethod()), expectedName),
|
||||
arguments(named(method2, findMethod(String.class)), expectedName),
|
||||
arguments(named(method3, findMethod(String.class, String.class)), expectedName),
|
||||
arguments(named(method4, findMethod(byte[].class)), expectedName));
|
||||
}
|
||||
|
||||
private static Method findMethod(Class<?>... paramTypes) {
|
||||
return ReflectionUtils.findMethod(CustomListener.class, "toUpperCase", paramTypes);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static class CustomListener {
|
||||
|
||||
// @JmsListener(...)
|
||||
String toUpperCase() {
|
||||
return "ENIGMA";
|
||||
}
|
||||
|
||||
// @JmsListener(...)
|
||||
String toUpperCase(String input) {
|
||||
return "ENIGMA";
|
||||
}
|
||||
|
||||
// @JmsListener(...)
|
||||
String toUpperCase(String input, String customHeader) {
|
||||
return "ENIGMA";
|
||||
}
|
||||
|
||||
// @JmsListener(...)
|
||||
String toUpperCase(byte[] input) {
|
||||
return "ENIGMA";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+16
-23
@@ -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,8 +17,10 @@
|
||||
package org.springframework.messaging.handler.invocation;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Parameter;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -28,16 +30,14 @@ import java.util.function.Consumer;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.aopalliance.intercept.MethodInterceptor;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.aop.framework.ProxyFactory;
|
||||
import org.springframework.aop.target.EmptyTargetSource;
|
||||
import org.springframework.cglib.core.SpringNamingPolicy;
|
||||
import org.springframework.cglib.proxy.Callback;
|
||||
import org.springframework.cglib.proxy.Enhancer;
|
||||
import org.springframework.cglib.proxy.Factory;
|
||||
import org.springframework.cglib.proxy.MethodInterceptor;
|
||||
import org.springframework.cglib.proxy.MethodProxy;
|
||||
import org.springframework.core.DefaultParameterNameDiscoverer;
|
||||
import org.springframework.core.MethodIntrospector;
|
||||
@@ -123,6 +123,7 @@ import static java.util.stream.Collectors.joining;
|
||||
* </pre>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Juergen Hoeller
|
||||
* @since 5.2
|
||||
*/
|
||||
public class ResolvableMethod {
|
||||
@@ -189,7 +190,6 @@ public class ResolvableMethod {
|
||||
|
||||
/**
|
||||
* Filter on method arguments with annotation.
|
||||
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final ArgResolver annot(Predicate<MethodParameter>... filter) {
|
||||
@@ -302,7 +302,6 @@ public class ResolvableMethod {
|
||||
|
||||
/**
|
||||
* Filter on annotated methods.
|
||||
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final Builder<T> annot(Predicate<Method>... filters) {
|
||||
@@ -313,7 +312,6 @@ public class ResolvableMethod {
|
||||
/**
|
||||
* Filter on methods annotated with the given annotation type.
|
||||
* @see #annot(Predicate[])
|
||||
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final Builder<T> annotPresent(Class<? extends Annotation>... annotationTypes) {
|
||||
@@ -530,7 +528,6 @@ public class ResolvableMethod {
|
||||
|
||||
/**
|
||||
* Filter on method arguments with annotations.
|
||||
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final ArgResolver annot(Predicate<MethodParameter>... filters) {
|
||||
@@ -542,7 +539,6 @@ public class ResolvableMethod {
|
||||
* Filter on method arguments that have the given annotations.
|
||||
* @param annotationTypes the annotation types
|
||||
* @see #annot(Predicate[])
|
||||
* See {@link org.springframework.web.method.MvcAnnotationPredicates}.
|
||||
*/
|
||||
@SafeVarargs
|
||||
public final ArgResolver annotPresent(Class<? extends Annotation>... annotationTypes) {
|
||||
@@ -615,16 +611,10 @@ public class ResolvableMethod {
|
||||
}
|
||||
|
||||
|
||||
private static class MethodInvocationInterceptor
|
||||
implements org.springframework.cglib.proxy.MethodInterceptor, MethodInterceptor {
|
||||
private static class MethodInvocationInterceptor implements MethodInterceptor, InvocationHandler {
|
||||
|
||||
private Method invokedMethod;
|
||||
|
||||
|
||||
Method getInvokedMethod() {
|
||||
return this.invokedMethod;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) {
|
||||
@@ -639,20 +629,23 @@ public class ResolvableMethod {
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Object invoke(org.aopalliance.intercept.MethodInvocation inv) throws Throwable {
|
||||
return intercept(inv.getThis(), inv.getMethod(), inv.getArguments(), null);
|
||||
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
|
||||
return intercept(proxy, method, args, null);
|
||||
}
|
||||
|
||||
Method getInvokedMethod() {
|
||||
return this.invokedMethod;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T initProxy(Class<?> type, MethodInvocationInterceptor interceptor) {
|
||||
Assert.notNull(type, "'type' must not be null");
|
||||
if (type.isInterface()) {
|
||||
ProxyFactory factory = new ProxyFactory(EmptyTargetSource.INSTANCE);
|
||||
factory.addInterface(type);
|
||||
factory.addInterface(Supplier.class);
|
||||
factory.addAdvice(interceptor);
|
||||
return (T) factory.getProxy();
|
||||
return (T) Proxy.newProxyInstance(type.getClassLoader(),
|
||||
new Class<?>[] {type, Supplier.class},
|
||||
interceptor);
|
||||
}
|
||||
|
||||
else {
|
||||
|
||||
+14
-10
@@ -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 org.springframework.util.ConcurrentReferenceHashMap;
|
||||
* @author Rod Johnson
|
||||
* @author Oliver Gierke
|
||||
* @author Mark Paluch
|
||||
* @author Sam Brannen
|
||||
* @since 2.0
|
||||
* @see javax.persistence.PersistenceContext
|
||||
* @see javax.persistence.PersistenceContextType#TRANSACTION
|
||||
@@ -74,9 +75,9 @@ public abstract class SharedEntityManagerCreator {
|
||||
|
||||
private static final Map<Class<?>, Class<?>[]> cachedQueryInterfaces = new ConcurrentReferenceHashMap<>(4);
|
||||
|
||||
private static final Set<String> transactionRequiringMethods = new HashSet<>(8);
|
||||
private static final Set<String> transactionRequiringMethods = new HashSet<>(6);
|
||||
|
||||
private static final Set<String> queryTerminatingMethods = new HashSet<>(8);
|
||||
private static final Set<String> queryTerminatingMethods = new HashSet<>(9);
|
||||
|
||||
static {
|
||||
transactionRequiringMethods.add("joinTransaction");
|
||||
@@ -86,12 +87,15 @@ public abstract class SharedEntityManagerCreator {
|
||||
transactionRequiringMethods.add("remove");
|
||||
transactionRequiringMethods.add("refresh");
|
||||
|
||||
queryTerminatingMethods.add("execute"); // JPA 2.1 StoredProcedureQuery
|
||||
queryTerminatingMethods.add("executeUpdate");
|
||||
queryTerminatingMethods.add("getSingleResult");
|
||||
queryTerminatingMethods.add("getResultStream");
|
||||
queryTerminatingMethods.add("getResultList");
|
||||
queryTerminatingMethods.add("list"); // Hibernate Query.list() method
|
||||
queryTerminatingMethods.add("execute"); // javax.persistence.StoredProcedureQuery.execute()
|
||||
queryTerminatingMethods.add("executeUpdate"); // javax.persistence.Query.executeUpdate()
|
||||
queryTerminatingMethods.add("getSingleResult"); // javax.persistence.Query.getSingleResult()
|
||||
queryTerminatingMethods.add("getResultStream"); // javax.persistence.Query.getResultStream()
|
||||
queryTerminatingMethods.add("getResultList"); // javax.persistence.Query.getResultList()
|
||||
queryTerminatingMethods.add("list"); // org.hibernate.query.Query.list()
|
||||
queryTerminatingMethods.add("stream"); // org.hibernate.query.Query.stream()
|
||||
queryTerminatingMethods.add("uniqueResult"); // org.hibernate.query.Query.uniqueResult()
|
||||
queryTerminatingMethods.add("uniqueResultOptional"); // org.hibernate.query.Query.uniqueResultOptional()
|
||||
}
|
||||
|
||||
|
||||
@@ -432,7 +436,7 @@ public abstract class SharedEntityManagerCreator {
|
||||
entry.setValue(storedProc.getOutputParameterValue(key.toString()));
|
||||
}
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
catch (RuntimeException ex) {
|
||||
entry.setValue(ex);
|
||||
}
|
||||
}
|
||||
|
||||
+25
-8
@@ -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.
|
||||
@@ -193,7 +193,7 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
Mono<Connection> newCon = Mono.from(obtainConnectionFactory().create());
|
||||
connectionMono = newCon.doOnNext(connection -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Acquired Connection [" + newCon + "] for R2DBC transaction");
|
||||
logger.debug("Acquired Connection [" + connection + "] for R2DBC transaction");
|
||||
}
|
||||
txObject.setConnectionHolder(new ConnectionHolder(connection), true);
|
||||
});
|
||||
@@ -325,31 +325,48 @@ public class R2dbcTransactionManager extends AbstractReactiveTransactionManager
|
||||
Mono<Void> afterCleanup = Mono.empty();
|
||||
|
||||
if (txObject.isMustRestoreAutoCommit()) {
|
||||
afterCleanup = afterCleanup.then(Mono.from(con.setAutoCommit(true)));
|
||||
Mono<Void> restoreAutoCommitStep = safeCleanupStep(
|
||||
"doCleanupAfterCompletion when restoring autocommit", Mono.from(con.setAutoCommit(true)));
|
||||
afterCleanup = afterCleanup.then(restoreAutoCommitStep);
|
||||
}
|
||||
|
||||
if (txObject.getPreviousIsolationLevel() != null) {
|
||||
afterCleanup = afterCleanup
|
||||
.then(Mono.from(con.setTransactionIsolationLevel(txObject.getPreviousIsolationLevel())));
|
||||
Mono<Void> restoreIsolationStep = safeCleanupStep(
|
||||
"doCleanupAfterCompletion when restoring isolation level",
|
||||
Mono.from(con.setTransactionIsolationLevel(txObject.getPreviousIsolationLevel())));
|
||||
afterCleanup = afterCleanup.then(restoreIsolationStep);
|
||||
}
|
||||
|
||||
return afterCleanup.then(Mono.defer(() -> {
|
||||
Mono<Void> releaseConnectionStep = Mono.defer(() -> {
|
||||
try {
|
||||
if (txObject.isNewConnectionHolder()) {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Releasing R2DBC Connection [" + con + "] after transaction");
|
||||
}
|
||||
return ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory());
|
||||
return safeCleanupStep("doCleanupAfterCompletion when releasing R2DBC Connection",
|
||||
ConnectionFactoryUtils.releaseConnection(con, obtainConnectionFactory()));
|
||||
}
|
||||
}
|
||||
finally {
|
||||
txObject.getConnectionHolder().clear();
|
||||
}
|
||||
return Mono.empty();
|
||||
}));
|
||||
});
|
||||
return afterCleanup.then(releaseConnectionStep);
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<Void> safeCleanupStep(String stepDescription, Mono<Void> stepMono) {
|
||||
if (!logger.isDebugEnabled()) {
|
||||
return stepMono.onErrorComplete();
|
||||
}
|
||||
else {
|
||||
return stepMono.doOnError(e ->
|
||||
logger.debug(String.format("Error ignored during %s: %s", stepDescription, e)))
|
||||
.onErrorComplete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare the transactional {@link Connection} right before transaction begin.
|
||||
* @deprecated in favor of {@link #prepareTransactionalConnection(Connection, TransactionDefinition)}
|
||||
|
||||
@@ -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.
|
||||
@@ -20,34 +20,18 @@ import java.util.function.Function;
|
||||
|
||||
import io.r2dbc.spi.Connection;
|
||||
|
||||
|
||||
/**
|
||||
* Union type combining {@link Function} and {@link SqlProvider} to expose the SQL that is
|
||||
* related to the underlying action.
|
||||
* related to the underlying action. The SqlProvider can support lazy / generate once semantics,
|
||||
* in which case {@link #getSql()} can be {@code null} until the {@code #apply(Connection)}
|
||||
* method is invoked.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Simon Baslé
|
||||
* @since 5.3
|
||||
* @param <R> the type of the result of the function.
|
||||
*/
|
||||
class ConnectionFunction<R> implements Function<Connection, R>, SqlProvider {
|
||||
|
||||
private final String sql;
|
||||
|
||||
private final Function<Connection, R> function;
|
||||
|
||||
|
||||
ConnectionFunction(String sql, Function<Connection, R> function) {
|
||||
this.sql = sql;
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R apply(Connection t) {
|
||||
return this.function.apply(t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSql() {
|
||||
return this.sql;
|
||||
}
|
||||
interface ConnectionFunction<R> extends Function<Connection, R>, SqlProvider {
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -79,7 +79,10 @@ public interface DatabaseClient extends ConnectionAccessor {
|
||||
* the execution. The SQL string can contain either native parameter
|
||||
* bind markers or named parameters (e.g. {@literal :foo, :bar}) when
|
||||
* {@link NamedParameterExpander} is enabled.
|
||||
* <p>Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}
|
||||
* <p>Accepts {@link PreparedOperation} as SQL and binding {@link Supplier}.
|
||||
* <p>{@code DatabaseClient} implementations should defer the resolution of
|
||||
* the SQL string as much as possible, ideally up to the point where a
|
||||
* {@code Subscription} happens. This is the case for the default implementation.
|
||||
* @param sqlSupplier a supplier for the SQL statement
|
||||
* @return a new {@link GenericExecuteSpec}
|
||||
* @see NamedParameterExpander
|
||||
|
||||
+23
-28
@@ -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.
|
||||
@@ -60,6 +60,7 @@ import org.springframework.util.StringUtils;
|
||||
* @author Mark Paluch
|
||||
* @author Mingyuan Wu
|
||||
* @author Bogdan Ilchyshyn
|
||||
* @author Simon Baslé
|
||||
* @since 5.3
|
||||
*/
|
||||
class DefaultDatabaseClient implements DatabaseClient {
|
||||
@@ -158,11 +159,9 @@ class DefaultDatabaseClient implements DatabaseClient {
|
||||
/**
|
||||
* Release the {@link Connection}.
|
||||
* @param connection to close.
|
||||
* @return a {@link Publisher} that completes successfully when the connection is
|
||||
* closed
|
||||
* @return a {@link Publisher} that completes successfully when the connection is closed
|
||||
*/
|
||||
private Publisher<Void> closeConnection(Connection connection) {
|
||||
|
||||
return ConnectionFactoryUtils.currentConnectionFactory(
|
||||
obtainConnectionFactory()).then().onErrorResume(Exception.class,
|
||||
e -> Mono.from(connection.close()));
|
||||
@@ -188,24 +187,22 @@ class DefaultDatabaseClient implements DatabaseClient {
|
||||
new CloseSuppressingInvocationHandler(con));
|
||||
}
|
||||
|
||||
private static Mono<Integer> sumRowsUpdated(
|
||||
Function<Connection, Flux<Result>> resultFunction, Connection it) {
|
||||
private static Mono<Integer> sumRowsUpdated(Function<Connection, Flux<Result>> resultFunction, Connection it) {
|
||||
return resultFunction.apply(it)
|
||||
.flatMap(Result::getRowsUpdated)
|
||||
.collect(Collectors.summingInt(Integer::intValue));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine SQL from potential provider object.
|
||||
* @param sqlProvider object that's potentially a SqlProvider
|
||||
* Get SQL from a potential provider object.
|
||||
* @param object an object that is potentially an SqlProvider
|
||||
* @return the SQL string, or {@code null}
|
||||
* @see SqlProvider
|
||||
*/
|
||||
@Nullable
|
||||
private static String getSql(Object sqlProvider) {
|
||||
|
||||
if (sqlProvider instanceof SqlProvider) {
|
||||
return ((SqlProvider) sqlProvider).getSql();
|
||||
private static String getSql(Object object) {
|
||||
if (object instanceof SqlProvider) {
|
||||
return ((SqlProvider) object).getSql();
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
@@ -214,7 +211,7 @@ class DefaultDatabaseClient implements DatabaseClient {
|
||||
|
||||
|
||||
/**
|
||||
* Base class for {@link DatabaseClient.GenericExecuteSpec} implementations.
|
||||
* Default {@link DatabaseClient.GenericExecuteSpec} implementation.
|
||||
*/
|
||||
class DefaultGenericExecuteSpec implements GenericExecuteSpec {
|
||||
|
||||
@@ -322,9 +319,8 @@ class DefaultDatabaseClient implements DatabaseClient {
|
||||
return fetch().rowsUpdated().then();
|
||||
}
|
||||
|
||||
private <T> FetchSpec<T> execute(Supplier<String> sqlSupplier, BiFunction<Row, RowMetadata, T> mappingFunction) {
|
||||
String sql = getRequiredSql(sqlSupplier);
|
||||
Function<Connection, Statement> statementFunction = connection -> {
|
||||
private ResultFunction getResultFunction(Supplier<String> sqlSupplier) {
|
||||
BiFunction<Connection, String, Statement> statementFunction = (connection, sql) -> {
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Executing SQL statement [" + sql + "]");
|
||||
}
|
||||
@@ -370,16 +366,16 @@ class DefaultDatabaseClient implements DatabaseClient {
|
||||
return statement;
|
||||
};
|
||||
|
||||
Function<Connection, Flux<Result>> resultFunction = connection -> {
|
||||
Statement statement = statementFunction.apply(connection);
|
||||
return Flux.from(this.filterFunction.filter(statement, DefaultDatabaseClient.this.executeFunction))
|
||||
.cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]");
|
||||
};
|
||||
return new ResultFunction(sqlSupplier, statementFunction, this.filterFunction, DefaultDatabaseClient.this.executeFunction);
|
||||
}
|
||||
|
||||
private <T> FetchSpec<T> execute(Supplier<String> sqlSupplier, BiFunction<Row, RowMetadata, T> mappingFunction) {
|
||||
ResultFunction resultHandler = getResultFunction(sqlSupplier);
|
||||
|
||||
return new DefaultFetchSpec<>(
|
||||
DefaultDatabaseClient.this, sql,
|
||||
new ConnectionFunction<>(sql, resultFunction),
|
||||
new ConnectionFunction<>(sql, connection -> sumRowsUpdated(resultFunction, connection)),
|
||||
DefaultDatabaseClient.this,
|
||||
resultHandler,
|
||||
connection -> sumRowsUpdated(resultHandler, connection),
|
||||
mappingFunction);
|
||||
}
|
||||
|
||||
@@ -505,12 +501,11 @@ class DefaultDatabaseClient implements DatabaseClient {
|
||||
|
||||
private static final long serialVersionUID = -8994138383301201380L;
|
||||
|
||||
final Connection connection;
|
||||
final transient Connection connection;
|
||||
|
||||
final Function<Connection, Publisher<Void>> closeFunction;
|
||||
final transient Function<Connection, Publisher<Void>> closeFunction;
|
||||
|
||||
ConnectionCloseHolder(Connection connection,
|
||||
Function<Connection, Publisher<Void>> closeFunction) {
|
||||
ConnectionCloseHolder(Connection connection, Function<Connection, Publisher<Void>> closeFunction) {
|
||||
this.connection = connection;
|
||||
this.closeFunction = closeFunction;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -20,7 +20,6 @@ import java.util.function.BiFunction;
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.Result;
|
||||
import io.r2dbc.spi.Row;
|
||||
import io.r2dbc.spi.RowMetadata;
|
||||
import reactor.core.publisher.Flux;
|
||||
@@ -32,6 +31,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException;
|
||||
* Default {@link FetchSpec} implementation.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Simon Baslé
|
||||
* @since 5.3
|
||||
* @param <T> the row result type
|
||||
*/
|
||||
@@ -39,24 +39,21 @@ class DefaultFetchSpec<T> implements FetchSpec<T> {
|
||||
|
||||
private final ConnectionAccessor connectionAccessor;
|
||||
|
||||
private final String sql;
|
||||
|
||||
private final Function<Connection, Flux<Result>> resultFunction;
|
||||
private final ResultFunction resultFunction;
|
||||
|
||||
private final Function<Connection, Mono<Integer>> updatedRowsFunction;
|
||||
|
||||
private final BiFunction<Row, RowMetadata, T> mappingFunction;
|
||||
|
||||
|
||||
DefaultFetchSpec(ConnectionAccessor connectionAccessor, String sql,
|
||||
Function<Connection, Flux<Result>> resultFunction,
|
||||
DefaultFetchSpec(ConnectionAccessor connectionAccessor,
|
||||
ResultFunction resultFunction,
|
||||
Function<Connection, Mono<Integer>> updatedRowsFunction,
|
||||
BiFunction<Row, RowMetadata, T> mappingFunction) {
|
||||
|
||||
this.sql = sql;
|
||||
this.connectionAccessor = connectionAccessor;
|
||||
this.resultFunction = resultFunction;
|
||||
this.updatedRowsFunction = updatedRowsFunction;
|
||||
this.updatedRowsFunction = new DelegateConnectionFunction<>(resultFunction, updatedRowsFunction);
|
||||
this.mappingFunction = mappingFunction;
|
||||
}
|
||||
|
||||
@@ -70,7 +67,7 @@ class DefaultFetchSpec<T> implements FetchSpec<T> {
|
||||
}
|
||||
if (list.size() > 1) {
|
||||
return Mono.error(new IncorrectResultSizeDataAccessException(
|
||||
String.format("Query [%s] returned non unique result.", this.sql),
|
||||
String.format("Query [%s] returned non unique result.", this.resultFunction.getSql()),
|
||||
1));
|
||||
}
|
||||
return Mono.just(list.get(0));
|
||||
@@ -84,7 +81,7 @@ class DefaultFetchSpec<T> implements FetchSpec<T> {
|
||||
|
||||
@Override
|
||||
public Flux<T> all() {
|
||||
return this.connectionAccessor.inConnectionMany(new ConnectionFunction<>(this.sql,
|
||||
return this.connectionAccessor.inConnectionMany(new DelegateConnectionFunction<>(this.resultFunction,
|
||||
connection -> this.resultFunction.apply(connection)
|
||||
.flatMap(result -> result.map(this.mappingFunction))));
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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.r2dbc.core;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import io.r2dbc.spi.Connection;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A {@link ConnectionFunction} that delegates to a {@code SqlProvider} and a plain
|
||||
* {@code Function}.
|
||||
*
|
||||
* @author Simon Baslé
|
||||
* @since 5.3.26
|
||||
* @param <R> the type of the result of the function.
|
||||
*/
|
||||
final class DelegateConnectionFunction<R> implements ConnectionFunction<R> {
|
||||
|
||||
private final SqlProvider sql;
|
||||
|
||||
private final Function<Connection, R> function;
|
||||
|
||||
|
||||
DelegateConnectionFunction(SqlProvider sql, Function<Connection, R> function) {
|
||||
this.sql = sql;
|
||||
this.function = function;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public R apply(Connection t) {
|
||||
return this.function.apply(t);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getSql() {
|
||||
return this.sql.getSql();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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.r2dbc.core;
|
||||
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.Result;
|
||||
import io.r2dbc.spi.Statement;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* A {@link ConnectionFunction} that produces a {@code Flux} of {@link Result} and that
|
||||
* defers generation of the SQL until the function has been applied.
|
||||
* Beforehand, the {@code getSql()} method simply returns {@code null}. The sql String is
|
||||
* also memoized during application, so that subsequent calls to {@link #getSql()} return
|
||||
* the same {@code String} without further calls to the {@code Supplier}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @author Simon Baslé
|
||||
* @since 5.3.26
|
||||
*/
|
||||
final class ResultFunction implements ConnectionFunction<Flux<Result>> {
|
||||
|
||||
final Supplier<String> sqlSupplier;
|
||||
final BiFunction<Connection, String, Statement> statementFunction;
|
||||
final StatementFilterFunction filterFunction;
|
||||
final ExecuteFunction executeFunction;
|
||||
|
||||
@Nullable
|
||||
String resolvedSql = null;
|
||||
|
||||
ResultFunction(Supplier<String> sqlSupplier, BiFunction<Connection, String, Statement> statementFunction, StatementFilterFunction filterFunction, ExecuteFunction executeFunction) {
|
||||
this.sqlSupplier = sqlSupplier;
|
||||
this.statementFunction = statementFunction;
|
||||
this.filterFunction = filterFunction;
|
||||
this.executeFunction = executeFunction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flux<Result> apply(Connection connection) {
|
||||
String sql = this.sqlSupplier.get();
|
||||
Assert.state(StringUtils.hasText(sql), "SQL returned by supplier must not be empty");
|
||||
this.resolvedSql = sql;
|
||||
Statement statement = this.statementFunction.apply(connection, sql);
|
||||
return Flux.from(this.filterFunction.filter(statement, this.executeFunction))
|
||||
.cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]");
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getSql() {
|
||||
return this.resolvedSql;
|
||||
}
|
||||
}
|
||||
+32
-1
@@ -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.
|
||||
@@ -22,12 +22,15 @@ import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
import io.r2dbc.spi.IsolationLevel;
|
||||
import io.r2dbc.spi.R2dbcBadGrammarException;
|
||||
import io.r2dbc.spi.R2dbcTimeoutException;
|
||||
import io.r2dbc.spi.Statement;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.r2dbc.BadSqlGrammarException;
|
||||
import org.springframework.transaction.CannotCreateTransactionException;
|
||||
import org.springframework.transaction.IllegalTransactionStateException;
|
||||
import org.springframework.transaction.TransactionDefinition;
|
||||
@@ -319,6 +322,34 @@ class R2dbcTransactionManagerUnitTests {
|
||||
verifyNoMoreInteractions(connectionMock);
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
void testConnectionReleasedWhenRollbackFails() {
|
||||
when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> Mono.error(new R2dbcBadGrammarException("Rollback should fail"))), Mono.empty());
|
||||
|
||||
TransactionalOperator operator = TransactionalOperator.create(tm);
|
||||
|
||||
when(connectionMock.isAutoCommit()).thenReturn(true);
|
||||
when(connectionMock.setAutoCommit(true)).thenReturn(Mono.defer(() -> Mono.error(new R2dbcTimeoutException("SET AUTOCOMMIT = 1 timed out"))));
|
||||
when(connectionMock.setTransactionIsolationLevel(any())).thenReturn(Mono.empty());
|
||||
when(connectionMock.setAutoCommit(false)).thenReturn(Mono.empty());
|
||||
|
||||
operator.execute(reactiveTransaction -> ConnectionFactoryUtils.getConnection(connectionFactoryMock)
|
||||
.doOnNext(connection -> {
|
||||
throw new IllegalStateException("Intentional error to trigger rollback");
|
||||
}).then()).as(StepVerifier::create)
|
||||
.verifyErrorSatisfies(e -> Assertions.assertThat(e)
|
||||
.isInstanceOf(BadSqlGrammarException.class)
|
||||
.hasCause(new R2dbcBadGrammarException("Rollback should fail"))
|
||||
);
|
||||
|
||||
verify(connectionMock).isAutoCommit();
|
||||
verify(connectionMock).beginTransaction();
|
||||
verify(connectionMock, never()).commitTransaction();
|
||||
verify(connectionMock).rollbackTransaction();
|
||||
verify(connectionMock).close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testTransactionSetRollbackOnly() {
|
||||
when(connectionMock.rollbackTransaction()).thenReturn(Mono.empty());
|
||||
|
||||
+45
-1
@@ -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.
|
||||
@@ -17,6 +17,8 @@
|
||||
package org.springframework.r2dbc.core;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import io.r2dbc.spi.Connection;
|
||||
import io.r2dbc.spi.ConnectionFactory;
|
||||
@@ -64,6 +66,7 @@ import static org.mockito.BDDMockito.when;
|
||||
* @author Mark Paluch
|
||||
* @author Ferdinand Jacobs
|
||||
* @author Jens Schauder
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||
@@ -397,6 +400,47 @@ class DefaultDatabaseClientUnitTests {
|
||||
inOrder.verifyNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sqlSupplierInvocationIsDeferredUntilSubscription() {
|
||||
// We'll have either 2 or 3 rows, depending on the subscription and the generated SQL
|
||||
MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata(
|
||||
MockColumnMetadata.builder().name("id").javaType(Integer.class).build()).build();
|
||||
final MockRow row1 = MockRow.builder().identified("id", Integer.class, 1).build();
|
||||
final MockRow row2 = MockRow.builder().identified("id", Integer.class, 2).build();
|
||||
final MockRow row3 = MockRow.builder().identified("id", Integer.class, 3).build();
|
||||
// Set up 2 mock statements
|
||||
mockStatementFor("SELECT id FROM test WHERE id < '3'", MockResult.builder()
|
||||
.rowMetadata(metadata)
|
||||
.row(row1, row2).build());
|
||||
mockStatementFor("SELECT id FROM test WHERE id < '4'", MockResult.builder()
|
||||
.rowMetadata(metadata)
|
||||
.row(row1, row2, row3).build());
|
||||
// Create the client
|
||||
DatabaseClient databaseClient = this.databaseClientBuilder.build();
|
||||
|
||||
AtomicInteger invoked = new AtomicInteger();
|
||||
// Assemble a publisher, but don't subscribe yet
|
||||
Mono<List<Integer>> operation = databaseClient
|
||||
.sql(() -> {
|
||||
int idMax = 2 + invoked.incrementAndGet();
|
||||
return String.format("SELECT id FROM test WHERE id < '%s'", idMax);
|
||||
})
|
||||
.map(r -> r.get("id", Integer.class))
|
||||
.all()
|
||||
.collectList();
|
||||
|
||||
assertThat(invoked).as("invoked (before subscription)").hasValue(0);
|
||||
|
||||
List<Integer> rows = operation.block();
|
||||
assertThat(invoked).as("invoked (after 1st subscription)").hasValue(1);
|
||||
assertThat(rows).containsExactly(1, 2);
|
||||
|
||||
rows = operation.block();
|
||||
assertThat(invoked).as("invoked (after 2nd subscription)").hasValue(2);
|
||||
assertThat(rows).containsExactly(1, 2, 3);
|
||||
}
|
||||
|
||||
|
||||
private Statement mockStatement() {
|
||||
return mockStatementFor(null, null);
|
||||
}
|
||||
|
||||
+5
-4
@@ -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.
|
||||
@@ -70,14 +70,15 @@ import java.lang.annotation.Target;
|
||||
* class ExampleIntegrationTests {
|
||||
*
|
||||
* @Container
|
||||
* static RedisContainer redis = new RedisContainer();
|
||||
* static GenericContainer redis =
|
||||
* new GenericContainer("redis:5.0.3-alpine").withExposedPorts(6379);
|
||||
*
|
||||
* // ...
|
||||
*
|
||||
* @DynamicPropertySource
|
||||
* static void redisProperties(DynamicPropertyRegistry registry) {
|
||||
* registry.add("redis.host", redis::getContainerIpAddress);
|
||||
* registry.add("redis.port", redis::getMappedPort);
|
||||
* registry.add("redis.host", redis::getHost);
|
||||
* registry.add("redis.port", redis::getFirstMappedPort);
|
||||
* }
|
||||
*
|
||||
* }</pre>
|
||||
|
||||
+121
-12
@@ -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.
|
||||
@@ -47,6 +47,7 @@ import static org.springframework.test.util.AssertionErrors.fail;
|
||||
* @author Craig Walls
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @author Simon Baslé
|
||||
* @since 3.2
|
||||
*/
|
||||
public abstract class MockRestRequestMatchers {
|
||||
@@ -112,8 +113,49 @@ public abstract class MockRestRequestMatchers {
|
||||
return request -> assertEquals("Unexpected request", uri, request.getURI());
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert request query parameter values with the given Hamcrest matcher,
|
||||
* matching on the entire {@link List} of values.
|
||||
* <p>For example, this can be used to check that the list of query parameter
|
||||
* values has at least one value matching a given Hamcrest matcher (such as
|
||||
* {@link org.hamcrest.Matchers#hasItem(Matcher)}), that every value in the list
|
||||
* matches common criteria (such as {@link org.hamcrest.Matchers#everyItem(Matcher)}),
|
||||
* that each value in the list matches corresponding dedicated criteria
|
||||
* (such as {@link org.hamcrest.Matchers#contains(Matcher[])}), etc.
|
||||
* @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.27
|
||||
* @see #queryParam(String, Matcher...)
|
||||
* @see #queryParam(String, String...)
|
||||
*/
|
||||
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);
|
||||
if (paramValues == null) {
|
||||
fail("Expected query param <" + name + "> to exist but was null");
|
||||
}
|
||||
assertThat("Query param [" + name + "] values", paramValues, matcher);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert request query parameter values with the given Hamcrest matcher(s).
|
||||
* <p>If the query parameter value list is larger than the number of provided
|
||||
* {@code matchers}, no matchers will be applied to the extra query parameter
|
||||
* 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 #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 #queryParamList(String, Matcher)
|
||||
* @see #queryParam(String, String...)
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static RequestMatcher queryParam(String name, Matcher<? super String>... matchers) {
|
||||
@@ -128,6 +170,20 @@ public abstract class MockRestRequestMatchers {
|
||||
|
||||
/**
|
||||
* Assert request query parameter values.
|
||||
* <p>If the query parameter value list is larger than the number of
|
||||
* {@code expectedValues}, no assertions will be applied to the extra query
|
||||
* 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 #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 #queryParamList(String, Matcher)
|
||||
* @see #queryParam(String, Matcher...)
|
||||
*/
|
||||
public static RequestMatcher queryParam(String name, String... expectedValues) {
|
||||
return request -> {
|
||||
@@ -143,21 +199,47 @@ public abstract class MockRestRequestMatchers {
|
||||
return UriComponentsBuilder.fromUri(request.getURI()).build().getQueryParams();
|
||||
}
|
||||
|
||||
private static void assertValueCount(
|
||||
String valueType, String name, MultiValueMap<String, String> map, int count) {
|
||||
|
||||
List<String> values = map.get(name);
|
||||
String message = "Expected " + valueType + " <" + name + ">";
|
||||
if (values == null) {
|
||||
fail(message + " to exist but was null");
|
||||
}
|
||||
if (count > values.size()) {
|
||||
fail(message + " to have at least <" + count + "> values but found " + values);
|
||||
}
|
||||
/**
|
||||
* Assert request header values with the given Hamcrest matcher, matching on
|
||||
* the entire {@link List} of values.
|
||||
* <p>For example, this can be used to check that the list of header values
|
||||
* has at least one value matching a given Hamcrest matcher (such as
|
||||
* {@link org.hamcrest.Matchers#hasItem(Matcher)}), that every value in the list
|
||||
* matches common criteria (such as {@link org.hamcrest.Matchers#everyItem(Matcher)}),
|
||||
* that each value in the list matches corresponding dedicated criteria
|
||||
* (such as {@link org.hamcrest.Matchers#contains(Matcher[])}), etc.
|
||||
* @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.27
|
||||
* @see #header(String, Matcher...)
|
||||
* @see #header(String, String...)
|
||||
*/
|
||||
public static RequestMatcher headerList(String name, Matcher<? super List<String>> matcher) {
|
||||
return request -> {
|
||||
List<String> headerValues = request.getHeaders().get(name);
|
||||
if (headerValues == null) {
|
||||
fail("Expected header <" + name + "> to exist but was null");
|
||||
}
|
||||
assertThat("Request header [" + name + "] values", headerValues, matcher);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert request header values with the given Hamcrest matcher(s).
|
||||
* <p>If the header value list is larger than the number of provided
|
||||
* {@code matchers}, no matchers will be applied to the extra header values,
|
||||
* 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 #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 #headerList(String, Matcher)
|
||||
* @see #header(String, String...)
|
||||
*/
|
||||
@SafeVarargs
|
||||
public static RequestMatcher header(String name, Matcher<? super String>... matchers) {
|
||||
@@ -173,6 +255,19 @@ public abstract class MockRestRequestMatchers {
|
||||
|
||||
/**
|
||||
* Assert request header values.
|
||||
* <p>If the header value list is larger than the number of {@code expectedValues},
|
||||
* no matchers will be applied to the extra header values, effectively ignoring the
|
||||
* 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 #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 #headerList(String, Matcher)
|
||||
* @see #header(String, Matcher...)
|
||||
*/
|
||||
public static RequestMatcher header(String name, String... expectedValues) {
|
||||
return request -> {
|
||||
@@ -258,4 +353,18 @@ public abstract class MockRestRequestMatchers {
|
||||
return new XpathRequestMatchers(expression, namespaces, args);
|
||||
}
|
||||
|
||||
|
||||
private static void assertValueCount(
|
||||
String valueType, String name, MultiValueMap<String, String> map, int count) {
|
||||
|
||||
List<String> values = map.get(name);
|
||||
String message = "Expected " + valueType + " <" + name + ">";
|
||||
if (values == null) {
|
||||
fail(message + " to exist but was null");
|
||||
}
|
||||
if (count > values.size()) {
|
||||
fail(message + " to have at least <" + count + "> values but found " + values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+9
-2
@@ -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.
|
||||
@@ -68,6 +68,8 @@ public final class MockMvcWebConnection implements WebConnection {
|
||||
|
||||
private WebClient webClient;
|
||||
|
||||
private static int MAX_FORWARDS = 100;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new instance that assumes the context path of the application
|
||||
@@ -133,10 +135,15 @@ public final class MockMvcWebConnection implements WebConnection {
|
||||
|
||||
MockHttpServletResponse httpServletResponse = getResponse(requestBuilder);
|
||||
String forwardedUrl = httpServletResponse.getForwardedUrl();
|
||||
while (forwardedUrl != null) {
|
||||
int forwards = 0;
|
||||
while (forwardedUrl != null && forwards < MAX_FORWARDS) {
|
||||
requestBuilder.setForwardPostProcessor(new ForwardRequestPostProcessor(forwardedUrl));
|
||||
httpServletResponse = getResponse(requestBuilder);
|
||||
forwardedUrl = httpServletResponse.getForwardedUrl();
|
||||
forwards += 1;
|
||||
}
|
||||
if (forwards == MAX_FORWARDS) {
|
||||
throw new IllegalStateException("Forwarded more than " + forwards + " times in a row, potential infinite forward loop");
|
||||
}
|
||||
storeCookies(webRequest, httpServletResponse.getCookies());
|
||||
|
||||
|
||||
+2
-2
@@ -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.
|
||||
@@ -83,7 +83,7 @@ public class XpathResultMatchers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response encoding if explicitly defined in the response, {code null} otherwise.
|
||||
* Get the response encoding if explicitly defined in the response, {@code null} otherwise.
|
||||
*/
|
||||
@Nullable
|
||||
private String getDefinedEncoding(MockHttpServletResponse response) {
|
||||
|
||||
+25
-1
@@ -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.
|
||||
@@ -224,6 +224,18 @@ fun MockMvc.multipart(urlTemplate: String, vararg vars: Any?, dsl: MockMultipart
|
||||
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL.
|
||||
*
|
||||
* @see MockMvcRequestBuilders.multipart
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3.26
|
||||
*/
|
||||
fun MockMvc.multipart(httpMethod: HttpMethod, urlTemplate: String, vararg vars: Any?, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl {
|
||||
val requestBuilder = MockMvcRequestBuilders.multipart(httpMethod, urlTemplate, *vars)
|
||||
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL.
|
||||
*
|
||||
@@ -235,3 +247,15 @@ fun MockMvc.multipart(uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Un
|
||||
val requestBuilder = MockMvcRequestBuilders.multipart(uri)
|
||||
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* [MockMvc] extension providing access to [MockMultipartHttpServletRequestDsl] Kotlin DSL.
|
||||
*
|
||||
* @see MockMvcRequestBuilders.multipart
|
||||
* @author Sebastien Deleuze
|
||||
* @since 5.3.26
|
||||
*/
|
||||
fun MockMvc.multipart(httpMethod: HttpMethod, uri: URI, dsl: MockMultipartHttpServletRequestDsl.() -> Unit = {}): ResultActionsDsl {
|
||||
val requestBuilder = MockMvcRequestBuilders.multipart(httpMethod, uri)
|
||||
return MockMultipartHttpServletRequestDsl(requestBuilder).apply(dsl).perform(this)
|
||||
}
|
||||
|
||||
+209
-62
@@ -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.
|
||||
@@ -16,18 +16,31 @@
|
||||
|
||||
package org.springframework.test.web.client.match;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.ThrowableTypeAssert;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.mock.http.client.MockClientHttpRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.hamcrest.Matchers.allOf;
|
||||
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.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.startsWith;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link MockRestRequestMatchers}.
|
||||
@@ -35,179 +48,313 @@ import static org.hamcrest.Matchers.containsString;
|
||||
* @author Craig Walls
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Sam Brannen
|
||||
* @author Simon Baslé
|
||||
*/
|
||||
public class MockRestRequestMatchersTests {
|
||||
class MockRestRequestMatchersTests {
|
||||
|
||||
private final MockClientHttpRequest request = new MockClientHttpRequest();
|
||||
|
||||
|
||||
@Test
|
||||
public void requestTo() throws Exception {
|
||||
void requestTo() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/bar"));
|
||||
|
||||
MockRestRequestMatchers.requestTo("http://www.foo.example/bar").match(this.request);
|
||||
}
|
||||
|
||||
@Test // SPR-15819
|
||||
public void requestToUriTemplate() throws Exception {
|
||||
void requestToUriTemplate() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/bar"));
|
||||
|
||||
MockRestRequestMatchers.requestToUriTemplate("http://www.foo.example/{bar}", "bar").match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestToNoMatch() throws Exception {
|
||||
void requestToNoMatch() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/bar"));
|
||||
|
||||
assertThatThrownBy(
|
||||
() -> MockRestRequestMatchers.requestTo("http://www.foo.example/wrong").match(this.request))
|
||||
.isInstanceOf(AssertionError.class);
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.requestTo("http://www.foo.example/wrong").match(this.request));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestToContains() throws Exception {
|
||||
void requestToContains() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/bar"));
|
||||
|
||||
MockRestRequestMatchers.requestTo(containsString("bar")).match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void method() throws Exception {
|
||||
void method() throws Exception {
|
||||
this.request.setMethod(HttpMethod.GET);
|
||||
|
||||
MockRestRequestMatchers.method(HttpMethod.GET).match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void methodNoMatch() throws Exception {
|
||||
void methodNoMatch() throws Exception {
|
||||
this.request.setMethod(HttpMethod.POST);
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.method(HttpMethod.GET).match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("expected:<GET> but was:<POST>");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.method(HttpMethod.GET).match(this.request))
|
||||
.withMessageContaining("expected:<GET> but was:<POST>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void header() throws Exception {
|
||||
void header() throws Exception {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||
|
||||
MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerDoesNotExist() throws Exception {
|
||||
void headerDoesNotExist() throws Exception {
|
||||
MockRestRequestMatchers.headerDoesNotExist(null).match(this.request);
|
||||
MockRestRequestMatchers.headerDoesNotExist("").match(this.request);
|
||||
MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request);
|
||||
|
||||
List<String> values = Arrays.asList("bar", "baz");
|
||||
this.request.getHeaders().put("foo", values);
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessage("Expected header <foo> not to exist, but it exists with values: " + values);
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.headerDoesNotExist("foo").match(this.request))
|
||||
.withMessage("Expected header <foo> not to exist, but it exists with values: " + values);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerMissing() throws Exception {
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("was null");
|
||||
void headerMissing() {
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
|
||||
.withMessageContaining("was null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerMissingValue() throws Exception {
|
||||
void headerMissingValue() {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bad").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("expected:<bad> but was:<bar>");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bad").match(this.request))
|
||||
.withMessageContaining("expected:<bad> but was:<bar>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerContains() throws Exception {
|
||||
void headerContains() throws Exception {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||
|
||||
MockRestRequestMatchers.header("foo", containsString("ba")).match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerContainsWithMissingHeader() throws Exception {
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("baz")).match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("but was null");
|
||||
void headerContainsWithMissingHeader() {
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("baz")).match(this.request))
|
||||
.withMessage("Expected header <foo> to exist but was null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headerContainsWithMissingValue() throws Exception {
|
||||
void headerContainsWithMissingValue() {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("bx")).match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("was \"bar\"");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.header("foo", containsString("bx")).match(this.request))
|
||||
.withMessageContaining("was \"bar\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headers() throws Exception {
|
||||
void headerListMissing() {
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.headerList("foo", hasSize(2)).match(this.request))
|
||||
.withMessage("Expected header <foo> to exist but was null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerListMatchers() throws IOException {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||
|
||||
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);
|
||||
|
||||
MockRestRequestMatchers.headerList("foo", notNullValue()).match(this.request);
|
||||
MockRestRequestMatchers.headerList("foo", is(anything())).match(this.request);
|
||||
MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), notNullValue())).match(this.request);
|
||||
|
||||
MockRestRequestMatchers.headerList("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void headerListContainsMismatch() {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar", "baz"));
|
||||
|
||||
assertThatAssertionError()
|
||||
.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.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.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"));
|
||||
|
||||
MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersWithMissingHeader() throws Exception {
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("but was null");
|
||||
void headersWithMissingHeader() {
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar").match(this.request))
|
||||
.withMessage("Expected header <foo> to exist but was null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void headersWithMissingValue() throws Exception {
|
||||
this.request.getHeaders().put("foo", Collections.singletonList("bar"));
|
||||
void headersWithMissingValue() {
|
||||
this.request.getHeaders().put("foo", Arrays.asList("bar"));
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("to have at least <2> values");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.header("foo", "bar", "baz").match(this.request))
|
||||
.withMessageContaining("to have at least <2> values");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryParam() throws Exception {
|
||||
void queryParam() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||
|
||||
MockRestRequestMatchers.queryParam("foo", "bar", "baz").match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryParamMissing() throws Exception {
|
||||
void queryParamMissing() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/a"));
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bar").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("but was null");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bar").match(this.request))
|
||||
.withMessage("Expected query param <foo> to exist but was null");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryParamMissingValue() throws Exception {
|
||||
void queryParamMissingValue() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bad").match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("expected:<bad> but was:<bar>");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", "bad").match(this.request))
|
||||
.withMessageContaining("expected:<bad> but was:<bar>");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryParamContains() throws Exception {
|
||||
void queryParamContains() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||
|
||||
MockRestRequestMatchers.queryParam("foo", containsString("ba")).match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queryParamContainsWithMissingValue() throws Exception {
|
||||
void queryParamContainsWithMissingValue() throws Exception {
|
||||
this.request.setURI(new URI("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||
|
||||
assertThatThrownBy(() -> MockRestRequestMatchers.queryParam("foo", containsString("bx")).match(this.request))
|
||||
.isInstanceOf(AssertionError.class)
|
||||
.hasMessageContaining("was \"bar\"");
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.queryParam("foo", containsString("bx")).match(this.request))
|
||||
.withMessageContaining("was \"bar\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryParamListMissing() {
|
||||
assertThatAssertionError()
|
||||
.isThrownBy(() -> MockRestRequestMatchers.queryParamList("foo", hasSize(2)).match(this.request))
|
||||
.withMessage("Expected query param <foo> to exist but was null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryParamListMatchers() throws IOException {
|
||||
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||
|
||||
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);
|
||||
|
||||
MockRestRequestMatchers.queryParamList("foo", notNullValue()).match(this.request);
|
||||
MockRestRequestMatchers.queryParamList("foo", is(anything())).match(this.request);
|
||||
MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), notNullValue())).match(this.request);
|
||||
|
||||
MockRestRequestMatchers.queryParamList("foo", allOf(notNullValue(), hasSize(2))).match(this.request);
|
||||
}
|
||||
|
||||
@Test
|
||||
void queryParamListContainsMismatch() {
|
||||
this.request.setURI(URI.create("http://www.foo.example/a?foo=bar&foo=baz"));
|
||||
|
||||
assertThatAssertionError()
|
||||
.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.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.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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-1
@@ -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.
|
||||
@@ -31,4 +31,9 @@ public class ForwardController {
|
||||
return "forward:/a";
|
||||
}
|
||||
|
||||
@RequestMapping("/infiniteForward")
|
||||
public String infiniteForward() {
|
||||
return "forward:/infiniteForward";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user