Compare commits

...

66 Commits

Author SHA1 Message Date
Spring Buildmaster 967637d763 Release v5.3.1 2020-11-10 08:59:43 +00:00
Rossen Stoyanchev 2899652d8e Fix checkstyle violation 2020-11-09 22:30:35 +00:00
Rossen Stoyanchev 560a504a83 Polishing in DefaultWebClient 2020-11-09 22:07:31 +00:00
Rossen Stoyanchev 79f79e9306 WebClient method to populate the Reactor Context
The alternative is to use a filter but this makes it a little easier
and also guarantees that it will be downstream from all filters
regardless of their order, and therefore the Context will be visible
to all of them.

Closes gh-25710
2020-11-09 22:07:31 +00:00
Juergen Hoeller bd2640a9d6 Strict nullability for field assignment 2020-11-09 21:33:32 +01:00
Juergen Hoeller 6825287360 Polishing 2020-11-09 18:18:33 +01:00
Sébastien Deleuze 737d77a739 Fix @Transactional support on functions returning Flow
Closes gh-26052
2020-11-09 17:45:28 +01:00
Sébastien Deleuze 1f13516528 Fix @Transactional support on suspending function
With this commit, @Transactional on suspending functions
returning a value now commits the transaction properly.

Closes gh-25998
2020-11-09 17:43:31 +01:00
Rossen Stoyanchev 6bb3ad793e Add isOpen to WebSocketSession in WebFlux
Closes gh-26043
2020-11-09 14:06:43 +00:00
Sébastien Deleuze c73cff8bad Use RxJava 2/3 fromPublisher() when possible in ReactiveAdapterRegistry
Closes gh-26051
2020-11-09 14:49:45 +01:00
Juergen Hoeller e592634290 Polishing 2020-11-09 13:32:41 +01:00
Juergen Hoeller b25637e8ad Make setTaskScheduler and setTcpClient chaining-friendly
Closes gh-26049
2020-11-09 13:32:34 +01:00
izeye 0c347769a2 Fix wrong reference in UrlPathHelper.removeSemicolonContentInternal()
This commit also changes to short-circuit when `slashIndex` is -1.
2020-11-09 13:08:44 +01:00
Juergen Hoeller 2497d4285f Detect existing DispatcherServlet strategy beans in parent context as well
Closes gh-25290
2020-11-09 12:39:09 +01:00
Sébastien Deleuze 32238cc996 Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:19:24 +01:00
Sébastien Deleuze d80dd50c9d Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:11:39 +01:00
Sébastien Deleuze bdf120f1ef Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:10:25 +01:00
Sébastien Deleuze e781d21097 Add CoroutinesAnnotationTransactionInterceptorTests
suspendingValueSuccess() currently fails due to an
unexpected rollback on transactional suspending functions
returning a value.

See gh-25998
2020-11-09 10:10:13 +01:00
Sébastien Deleuze 8cef13cc30 Upgrade to Kotlin Coroutines 1.4.1 2020-11-09 09:34:46 +01:00
Rüdiger Schulz 2214ff8ce0 Fix Kotlin example code which does not compile
This kotlin code does not compile:
Overload resolution ambiguity. All these functions match.
default <R> RowsFetchSpec<R> map(Function<Row, R> mappingFunction)
<R> RowsFetchSpec<R> map(BiFunction<Row, RowMetadata, R> mappingFunction);

Closes gh-26016
2020-11-08 15:43:30 +01:00
Sam Brannen 411539ffef Update copyright date
See gh-26044
2020-11-08 15:41:31 +01:00
NaNRailgun 11aa901634 Delete explicit type arguments that can be inferred
Closes gh-26044
2020-11-08 15:40:01 +01:00
Rossen Stoyanchev 6e51370490 AbstractJackson2Encoder support for MappingJacksonValue
Closes gh-26035
2020-11-06 20:46:35 +00:00
Rossen Stoyanchev bcd2b9a8a7 Further refine logging in StompErrorHandler
In 5.3.x and forward, completely avoid logging unsent CONNECT messages
that are most likely authentication issues before the session is even
established.

Closes gh-26026
2020-11-05 21:50:12 +00:00
Rossen Stoyanchev 17dd7785f6 Refine logging in StompErrorHandler
Avoid a full stacktrace at ERROR level for a client message that could
not be sent to a MessageChannel.

See gh-26026
2020-11-05 21:34:56 +00:00
Juergen Hoeller 21f9d05bd9 Upgrade to Jetty 9.4.34, AssertJ 3.18, Mockito 3.6 2020-11-05 19:21:54 +01:00
Juergen Hoeller 19911af30a Polishing 2020-11-05 19:09:58 +01:00
Juergen Hoeller 6acb091c4e Upgrade SpelCompiler bytecode level to 1.8 and optimize for concurrent access
Closes gh-26033
2020-11-05 17:44:40 +01:00
Juergen Hoeller 99ed01e3f7 Optimize locking in AspectJProxyFactory for concurrent aspect instantiation
Closes gh-26034
2020-11-05 13:11:16 +01:00
Juergen Hoeller 2657757566 Separate assignment statements for local variable and instance field
See gh-25899
2020-11-05 13:10:45 +01:00
Сергей Цыпанов e1f51b4bf8 Avoid multiple volatile reads/writes in a row where only one is enough 2020-11-05 12:25:24 +01:00
Juergen Hoeller f2f84bfa7a Polishing 2020-11-05 00:04:33 +01:00
Juergen Hoeller 6d67bdf04f Polishing 2020-11-04 23:43:43 +01:00
Juergen Hoeller 97c8628bd6 Suppress NotWritablePropertyException in case of ignoreUnknown=true
Closes gh-25986
2020-11-04 23:42:37 +01:00
Juergen Hoeller 990a9c74b9 Restore removal of trailing semicolon content
See gh-26012
2020-11-04 23:42:28 +01:00
Juergen Hoeller b815accca9 Upgrade to Hibernate ORM 5.4.23 2020-11-04 16:52:40 +01:00
Juergen Hoeller 4e8d6bea4d Fix test code formatting 2020-11-04 16:52:11 +01:00
Juergen Hoeller 079ca80854 SpEL supports record-style accessor methods as well
Closes gh-26029
2020-11-04 16:51:54 +01:00
Juergen Hoeller 412aa06d86 Reliably refresh metadata for dynamically changing prototype bean class
Closes gh-26019
2020-11-04 16:48:54 +01:00
Juergen Hoeller d5b3e65718 Add since tag
See gh-26025
2020-11-04 16:46:44 +01:00
Juergen Hoeller a4679b87b9 Fix Checkstyle violation and rearrange getAdvisorCount declaration
See gh-26017
2020-11-04 16:43:31 +01:00
Erik van Paassen fbf8e37f94 Add FullyQualifiedAnnotationBeanNameGenerator.INSTANCE
Adds a static INSTANCE to FullyQualifiedAnnotationBeanNameGenerator,
just like its parent class, AnnotationBeanNameGenerator, does.

The class doesn't have any state and overriding/hiding the INSTANCE of
the superclass, this prevents unintended use of a regular
AnnotationBeanNameGenerator through
FullyQualifiedAnnotationBeanNameGenerator.INSTANCE.
2020-11-04 16:29:31 +01:00
Сергей Цыпанов 8c3cab7ead Improve usage of AdvisedSupport.getAdvisors() 2020-11-04 16:29:09 +01:00
Сергей Цыпанов 0015fd6734 Improve URI/query strings sanitization 2020-11-04 16:28:51 +01:00
Sam Brannen b077e4cd85 Use MergedAnnotationCollectors again in TestContextAnnotationUtils
See gh-26031
2020-11-04 14:42:19 +01:00
Sam Brannen 0a0299693b Do not create intermediate list in MergedAnnotationCollectors.toAnnotationSet()
Prior to this commit, MergedAnnotationCollectors.toAnnotationSet()
created an intermediate ArrayList for storing the results prior to
creating a LinkedHashSet in the finishing step.

Since the creation of the intermediate list is unnecessary, this commit
simplifies the implementation of toAnnotationSet() by using the
Collector.of() factory method that does not accept a `finisher` argument.
The resulting Collector internally uses a `castingIdentity()` function
as the `finisher`.

Closes gh-26031
2020-11-04 14:19:58 +01:00
Rossen Stoyanchev 41bdde5d6b Polishing 2020-11-03 19:31:02 +00:00
Rossen Stoyanchev 2e2d662158 Add toEntityFlux methods
Closes gh-26023
2020-11-03 19:31:02 +00:00
Sam Brannen 68934f1b79 Use TYPE_HIERARCHY strategy in AnnoDescr.findAllLocalMergedAnnotations()
Prior to this commit, the findAllLocalMergedAnnotations() method in
AnnotationDescriptor altered between the use of TYPE_HIERARCHY and
TYPE_HIERARCHY_AND_ENCLOSING_CLASSES for the SearchStrategy, depending
on @NestedTestConfiguration semantics; however, when searching for
"local" annotations, there is no need to search the enclosing class
hierarchy since AnnotationDescriptor#next() handles that use case.

This commit therefore switches to using only the TYPE_HIERARCHY
strategy.

This commit also discontinues the use of
MergedAnnotationCollectors.toAnnotationSet() in order to avoid the
unnecessary creation of a temporary List when collecting synthesized
annotations in a LinkedHashSet.

Closes gh-25985
2020-11-03 16:17:15 +01:00
hzmpay f781c459b4 Simplify AbstractBeanFactory.registerCustomEditors()
This commit avoids an unnecessary non-null check in
registerCustomEditors().

Closes gh-26022
2020-11-03 14:52:23 +01:00
Sam Brannen 5628c8cd63 Polish ActiveProfilesUtils 2020-11-03 14:46:17 +01:00
Rossen Stoyanchev 0f6038af70 Log can[Se]Deserialize error in Jackson codecs
Closes gh-25892
2020-11-02 17:25:57 +00:00
Rossen Stoyanchev 3eb2c5e22f Use static accessors in DefaultSimpUserRegistry
Closes gh-26010
2020-11-02 17:25:57 +00:00
Brian Clozel 3f722a9c7f Set DEBUG logs for release tasks in CI pipeline
This commit configures the logging level to DEBUG for the concourse
release scripts tasks.

This is useful to get a clue on where those long-running tasks are at.
2020-11-02 17:17:02 +01:00
Sam Brannen acbbf61be8 Preserve registration order in @ActiveProfiles
With this commit, bean definition profiles declared via @ActiveProfiles
are once again stored in registration order, in order to support use
cases in Spring Boot and other frameworks that depend on the
registration order.

This effectively reverts the changes made in conjunction with gh-25973.

Closes gh-26004
2020-11-02 16:39:59 +01:00
Sam Brannen 154f0c71c9 Polishing 2020-11-02 15:47:15 +01:00
Rossen Stoyanchev 7be7e5beb4 Ensure response not closed by MappingJackson2HttpMessageConverter
Closes gh-25987
2020-10-30 18:31:30 +00:00
Stephane Nicoll 37504e75e9 Merge pull request #25999 from izeye
* pr/25999:
  Fix a broken Asciidoctor syntax in core-resources.adoc

Closes gh-25999
2020-10-30 09:33:00 +01:00
izeye b89b37729c Fix a broken Asciidoctor syntax in core-resources.adoc
See gh-25999
2020-10-30 09:32:42 +01:00
Rossen Stoyanchev 141c79bc8f Default resolution of non-annotated Principal argument
Closes gh-25981
2020-10-29 21:29:35 +00:00
Arjen Poutsma 885a5048c1 Copy default headers, cookies in WebClient builder
This commit makes copies of the default headers and cookies when a
WebClient is built, so that subsequent changes to these do not affect
previously built clients.

Closes: gh-25992
2020-10-29 13:37:25 +01:00
Arjen Poutsma e981bf34b3 Removed unused sample code 2020-10-29 13:37:25 +01:00
Aakarshit Uppal 44239b2d7b Remove rogue semicolon 2020-10-29 12:32:20 +00:00
Stephane Nicoll a00c483927 Merge branch '5.2.x' 2020-10-27 16:56:22 +01:00
Spring Buildmaster 96ffa14141 Next development version (v5.3.1-SNAPSHOT) 2020-10-27 14:52:47 +00:00
Spring Buildmaster a9dec6a4af Next Development Version 2020-10-27 13:48:52 +00:00
88 changed files with 1571 additions and 785 deletions
+5 -5
View File
@@ -30,9 +30,9 @@ configure(allprojects) { project ->
mavenBom "io.projectreactor:reactor-bom:2020.0.0"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR8"
mavenBom "io.rsocket:rsocket-bom:1.1.0"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.33.v20201020"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.34.v20201102"
mavenBom "org.jetbrains.kotlin:kotlin-bom:1.4.10"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.0"
mavenBom "org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.4.1"
mavenBom "org.junit:junit-bom:5.7.0"
}
dependencies {
@@ -123,7 +123,7 @@ configure(allprojects) { project ->
dependency "net.sf.ehcache:ehcache:2.10.6"
dependency "org.ehcache:jcache:1.0.1"
dependency "org.ehcache:ehcache:3.4.0"
dependency "org.hibernate:hibernate-core:5.4.22.Final"
dependency "org.hibernate:hibernate-core:5.4.23.Final"
dependency "org.hibernate:hibernate-validator:6.1.6.Final"
dependency "org.webjars:webjars-locator-core:0.46"
dependency "org.webjars:underscorejs:1.8.3"
@@ -190,14 +190,14 @@ configure(allprojects) { project ->
dependency "org.testng:testng:7.3.0"
dependency "org.hamcrest:hamcrest:2.1"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.17.2"
dependency "org.assertj:assertj-core:3.18.0"
dependencySet(group: 'org.xmlunit', version: '2.6.2') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
}
dependencySet(group: 'org.mockito', version: '3.5.15') {
dependencySet(group: 'org.mockito', version: '3.6.0') {
entry('mockito-core') {
exclude group: "org.hamcrest", name: "hamcrest-core"
}
+6
View File
@@ -1,3 +1,9 @@
logging:
level:
io.spring.concourse: DEBUG
distribute:
optional-deployments:
- '.*\\.zip'
spring:
main:
banner-mode: off
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.0-SNAPSHOT
version=5.3.1
org.gradle.jvmargs=-Xmx1536M
org.gradle.caching=true
org.gradle.parallel=true
@@ -221,10 +221,12 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint,
@Override
@Nullable
public String[] getParameterNames() {
if (this.parameterNames == null) {
this.parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
String[] parameterNames = this.parameterNames;
if (parameterNames == null) {
parameterNames = parameterNameDiscoverer.getParameterNames(getMethod());
this.parameterNames = parameterNames;
}
return this.parameterNames;
return parameterNames;
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -160,23 +160,12 @@ public class AspectJProxyFactory extends ProxyCreatorSupport {
}
/**
* Get the singleton aspect instance for the supplied aspect type. An instance
* is created if one cannot be found in the instance cache.
* Get the singleton aspect instance for the supplied aspect type.
* An instance is created if one cannot be found in the instance cache.
*/
private Object getSingletonAspectInstance(Class<?> aspectClass) {
// Quick check without a lock...
Object instance = aspectCache.get(aspectClass);
if (instance == null) {
synchronized (aspectCache) {
// To be safe, check within full lock now...
instance = aspectCache.get(aspectClass);
if (instance == null) {
instance = new SimpleAspectInstanceFactory(aspectClass).getAspectInstance();
aspectCache.put(aspectClass, instance);
}
}
}
return instance;
return aspectCache.computeIfAbsent(aspectClass,
clazz -> new SimpleAspectInstanceFactory(clazz).getAspectInstance());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -114,6 +114,15 @@ public interface Advised extends TargetClassAware {
*/
Advisor[] getAdvisors();
/**
* Return the number of advisors applying to this proxy.
* <p>The default implementation delegates to {@code getAdvisors().length}.
* @since 5.3.1
*/
default int getAdvisorCount() {
return getAdvisors().length;
}
/**
* Add an advisor at the end of the advisor chain.
* <p>The Advisor may be an {@link org.springframework.aop.IntroductionAdvisor},
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -95,12 +95,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
*/
private List<Advisor> advisors = new ArrayList<>();
/**
* Array updated on changes to the advisors list, which is easier
* to manipulate internally.
*/
private Advisor[] advisorArray = new Advisor[0];
/**
* No-arg constructor for use as a JavaBean.
@@ -244,7 +238,12 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
@Override
public final Advisor[] getAdvisors() {
return this.advisorArray;
return this.advisors.toArray(new Advisor[0]);
}
@Override
public int getAdvisorCount() {
return this.advisors.size();
}
@Override
@@ -292,7 +291,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
}
}
updateAdvisorArray();
adviceChanged();
}
@@ -339,7 +337,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
Assert.notNull(advisor, "Advisor must not be null");
this.advisors.add(advisor);
}
updateAdvisorArray();
adviceChanged();
}
}
@@ -363,27 +360,18 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
"Illegal position " + pos + " in advisor list with size " + this.advisors.size());
}
this.advisors.add(pos, advisor);
updateAdvisorArray();
adviceChanged();
}
/**
* Bring the array up to date with the list.
*/
protected final void updateAdvisorArray() {
this.advisorArray = this.advisors.toArray(new Advisor[0]);
}
/**
* Allows uncontrolled access to the {@link List} of {@link Advisor Advisors}.
* <p>Use with care, and remember to {@link #updateAdvisorArray() refresh the advisor array}
* and {@link #adviceChanged() fire advice changed events} when making any modifications.
* <p>Use with care, and remember to {@link #adviceChanged() fire advice changed events}
* when making any modifications.
*/
protected final List<Advisor> getAdvisorsInternal() {
return this.advisors;
}
@Override
public void addAdvice(Advice advice) throws AopConfigException {
int pos = this.advisors.size();
@@ -521,7 +509,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
Assert.notNull(advisor, "Advisor must not be null");
this.advisors.add(advisor);
}
updateAdvisorArray();
adviceChanged();
}
@@ -536,7 +523,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
copy.advisorChainFactory = this.advisorChainFactory;
copy.interfaces = this.interfaces;
copy.advisors = this.advisors;
copy.updateAdvisorArray();
return copy;
}
@@ -553,7 +539,6 @@ public class AdvisedSupport extends ProxyConfig implements Advised {
this.methodCache = new ConcurrentHashMap<>(32);
}
@Override
public String toProxyConfigString() {
return toString();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -206,7 +206,7 @@ public abstract class AopProxyUtils {
* Check equality of the advisors behind the given AdvisedSupport objects.
*/
public static boolean equalsAdvisors(AdvisedSupport a, AdvisedSupport b) {
return Arrays.equals(a.getAdvisors(), b.getAdvisors());
return a.getAdvisorCount() == b.getAdvisorCount() && Arrays.equals(a.getAdvisors(), b.getAdvisors());
}
@@ -125,7 +125,7 @@ class CglibAopProxy implements AopProxy, Serializable {
*/
public CglibAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null");
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
throw new AopConfigException("No advisors and no TargetSource specified");
}
this.advised = config;
@@ -942,11 +942,11 @@ class CglibAopProxy implements AopProxy, Serializable {
}
// Advice instance identity is unimportant to the proxy class:
// All that matters is type and ordering.
Advisor[] thisAdvisors = this.advised.getAdvisors();
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
if (thisAdvisors.length != thatAdvisors.length) {
if (this.advised.getAdvisorCount() != otherAdvised.getAdvisorCount()) {
return false;
}
Advisor[] thisAdvisors = this.advised.getAdvisors();
Advisor[] thatAdvisors = otherAdvised.getAdvisors();
for (int i = 0; i < thisAdvisors.length; i++) {
Advisor thisAdvisor = thisAdvisors[i];
Advisor thatAdvisor = thatAdvisors[i];
@@ -104,7 +104,7 @@ final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializa
*/
public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException {
Assert.notNull(config, "AdvisedSupport must not be null");
if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
if (config.getAdvisorCount() == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) {
throw new AopConfigException("No advisors and no TargetSource specified");
}
this.advised = config;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -422,9 +422,12 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA
}
return;
}
else {
throw createNotWritablePropertyException(tokens.canonicalName);
if (this.suppressNotWritablePropertyException) {
// Optimization for common ignoreUnknown=true scenario since the
// exception would be caught and swallowed higher up anyway...
return;
}
throw createNotWritablePropertyException(tokens.canonicalName);
}
Object oldValue = null;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -40,6 +40,8 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
private boolean autoGrowNestedPaths = false;
boolean suppressNotWritablePropertyException = false;
@Override
public void setExtractOldValueForEditor(boolean extractOldValueForEditor) {
@@ -89,30 +91,41 @@ public abstract class AbstractPropertyAccessor extends TypeConverterSupport impl
List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues ?
((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));
for (PropertyValue pv : propertyValues) {
try {
// This method may throw any BeansException, which won't be caught
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = true;
}
try {
for (PropertyValue pv : propertyValues) {
// setPropertyValue may throw any BeansException, which won't be caught
// here, if there is a critical failure such as no matching field.
// We can attempt to deal only with less serious exceptions.
setPropertyValue(pv);
}
catch (NotWritablePropertyException ex) {
if (!ignoreUnknown) {
throw ex;
try {
setPropertyValue(pv);
}
// Otherwise, just ignore it and continue...
}
catch (NullValueInNestedPathException ex) {
if (!ignoreInvalid) {
throw ex;
catch (NotWritablePropertyException ex) {
if (!ignoreUnknown) {
throw ex;
}
// Otherwise, just ignore it and continue...
}
// Otherwise, just ignore it and continue...
}
catch (PropertyAccessException ex) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList<>();
catch (NullValueInNestedPathException ex) {
if (!ignoreInvalid) {
throw ex;
}
// Otherwise, just ignore it and continue...
}
propertyAccessExceptions.add(ex);
catch (PropertyAccessException ex) {
if (propertyAccessExceptions == null) {
propertyAccessExceptions = new ArrayList<>();
}
propertyAccessExceptions.add(ex);
}
}
}
finally {
if (ignoreUnknown) {
this.suppressNotWritablePropertyException = false;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -92,8 +92,7 @@ public class DirectFieldAccessor extends AbstractNestablePropertyAccessor {
@Override
protected NotWritablePropertyException createNotWritablePropertyException(String propertyName) {
PropertyMatches matches = PropertyMatches.forField(propertyName, getRootClass());
throw new NotWritablePropertyException(
getRootClass(), getNestedPath() + propertyName,
throw new NotWritablePropertyException(getRootClass(), getNestedPath() + propertyName,
matches.buildErrorMessage(), matches.getPossibleMatches());
}
@@ -644,21 +644,20 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
}
synchronized (this) {
if (!this.cached) {
Object cachedFieldValue = null;
if (value != null || this.required) {
this.cachedFieldValue = desc;
cachedFieldValue = desc;
registerDependentBeans(beanName, autowiredBeanNames);
if (autowiredBeanNames.size() == 1) {
String autowiredBeanName = autowiredBeanNames.iterator().next();
if (beanFactory.containsBean(autowiredBeanName) &&
beanFactory.isTypeMatch(autowiredBeanName, field.getType())) {
this.cachedFieldValue = new ShortcutDependencyDescriptor(
cachedFieldValue = new ShortcutDependencyDescriptor(
desc, autowiredBeanName, field.getType());
}
}
}
else {
this.cachedFieldValue = null;
}
this.cachedFieldValue = cachedFieldValue;
this.cached = true;
}
}
@@ -26,9 +26,6 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.MutablePropertyValues;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -69,8 +66,6 @@ public class InjectionMetadata {
};
private static final Log logger = LogFactory.getLog(InjectionMetadata.class);
private final Class<?> targetClass;
private final Collection<InjectedElement> injectedElements;
@@ -110,9 +105,6 @@ public class InjectionMetadata {
if (!beanDefinition.isExternallyManagedConfigMember(member)) {
beanDefinition.registerExternallyManagedConfigMember(member);
checkedElements.add(element);
if (logger.isTraceEnabled()) {
logger.trace("Registered injected element on class [" + this.targetClass.getName() + "]: " + element);
}
}
}
this.checkedElements = checkedElements;
@@ -124,9 +116,6 @@ public class InjectionMetadata {
(checkedElements != null ? checkedElements : this.injectedElements);
if (!elementsToIterate.isEmpty()) {
for (InjectedElement element : elementsToIterate) {
if (logger.isTraceEnabled()) {
logger.trace("Processing injected element of bean '" + beanName + "': " + element);
}
element.inject(target, beanName, pvs);
}
}
@@ -157,7 +146,8 @@ public class InjectionMetadata {
* @since 5.2
*/
public static InjectionMetadata forElements(Collection<InjectedElement> elements, Class<?> clazz) {
return (elements.isEmpty() ? InjectionMetadata.EMPTY : new InjectionMetadata(clazz, elements));
return (elements.isEmpty() ? new InjectionMetadata(clazz, Collections.emptyList()) :
new InjectionMetadata(clazz, elements));
}
/**
@@ -1288,10 +1288,8 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp
* @param registry the PropertyEditorRegistry to initialize
*/
protected void registerCustomEditors(PropertyEditorRegistry registry) {
PropertyEditorRegistrySupport registrySupport =
(registry instanceof PropertyEditorRegistrySupport ? (PropertyEditorRegistrySupport) registry : null);
if (registrySupport != null) {
registrySupport.useConfigValueEditors();
if (registry instanceof PropertyEditorRegistrySupport) {
((PropertyEditorRegistrySupport) registry).useConfigValueEditors();
}
if (!this.propertyEditorRegistrars.isEmpty()) {
for (PropertyEditorRegistrar registrar : this.propertyEditorRegistrars) {
@@ -155,8 +155,7 @@ public abstract class AbstractCacheManager implements CacheManager, Initializing
* @param name the name of the cache to be added
*/
private void updateCacheNames(String name) {
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames.size() + 1);
cacheNames.addAll(this.cacheNames);
Set<String> cacheNames = new LinkedHashSet<>(this.cacheNames);
cacheNames.add(name);
this.cacheNames = Collections.unmodifiableSet(cacheNames);
}
@@ -99,7 +99,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo
* @see #setBeanNameGenerator
*/
public static final AnnotationBeanNameGenerator IMPORT_BEAN_NAME_GENERATOR =
new FullyQualifiedAnnotationBeanNameGenerator();
FullyQualifiedAnnotationBeanNameGenerator.INSTANCE;
private static final String IMPORT_REGISTRY_BEAN_NAME =
ConfigurationClassPostProcessor.class.getName() + ".importRegistry";
@@ -43,6 +43,15 @@ import org.springframework.util.Assert;
*/
public class FullyQualifiedAnnotationBeanNameGenerator extends AnnotationBeanNameGenerator {
/**
* A convenient constant for a default {@code FullyQualifiedAnnotationBeanNameGenerator}
* instance, as used for configuration-level import purposes.
* @since 5.2.11
*/
public static final FullyQualifiedAnnotationBeanNameGenerator INSTANCE =
new FullyQualifiedAnnotationBeanNameGenerator();
@Override
protected String buildDefaultBeanName(BeanDefinition definition) {
String beanClassName = definition.getBeanClassName();
@@ -307,6 +307,23 @@ public class ConfigurationClassProcessingTests {
assertThat(tb.getLawyer()).isEqualTo(ctx.getBean(NestedTestBean.class));
}
@Test // gh-26019
public void autowiringWithDynamicPrototypeBeanClass() {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(
ConfigWithDynamicPrototype.class, PrototypeDependency.class);
PrototypeInterface p1 = ctx.getBean(PrototypeInterface.class, 1);
assertThat(p1).isInstanceOf(PrototypeOne.class);
assertThat(((PrototypeOne) p1).prototypeDependency).isNotNull();
PrototypeInterface p2 = ctx.getBean(PrototypeInterface.class, 2);
assertThat(p2).isInstanceOf(PrototypeTwo.class);
PrototypeInterface p3 = ctx.getBean(PrototypeInterface.class, 1);
assertThat(p3).isInstanceOf(PrototypeOne.class);
assertThat(((PrototypeOne) p3).prototypeDependency).isNotNull();
}
/**
* Creates a new {@link BeanFactory}, populates it with a {@link BeanDefinition}
@@ -632,4 +649,42 @@ public class ConfigurationClassProcessingTests {
}
}
static class PrototypeDependency {
}
interface PrototypeInterface {
}
static class PrototypeOne extends AbstractPrototype {
@Autowired
PrototypeDependency prototypeDependency;
}
static class PrototypeTwo extends AbstractPrototype {
// no autowired dependency here, in contrast to above
}
static class AbstractPrototype implements PrototypeInterface {
}
@Configuration
static class ConfigWithDynamicPrototype {
@Bean
@Scope(value = "prototype")
public PrototypeInterface getDemoBean( int i) {
switch ( i) {
case 1: return new PrototypeOne();
case 2:
default:
return new PrototypeTwo();
}
}
}
}
@@ -264,14 +264,12 @@ public class ReactiveAdapterRegistry {
registry.registerReactiveType(
ReactiveTypeDescriptor.multiValue(io.reactivex.Observable.class, io.reactivex.Observable::empty),
source -> ((io.reactivex.Observable<?>) source).toFlowable(io.reactivex.BackpressureStrategy.BUFFER),
source -> io.reactivex.Flowable.fromPublisher(source)
.toObservable()
io.reactivex.Observable::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.Single.class),
source -> ((io.reactivex.Single<?>) source).toFlowable(),
source -> io.reactivex.Flowable.fromPublisher(source)
.toObservable().singleElement().toSingle()
io.reactivex.Single::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleOptionalValue(io.reactivex.Maybe.class, io.reactivex.Maybe::empty),
@@ -282,8 +280,7 @@ public class ReactiveAdapterRegistry {
registry.registerReactiveType(
ReactiveTypeDescriptor.noValue(io.reactivex.Completable.class, io.reactivex.Completable::complete),
source -> ((io.reactivex.Completable) source).toFlowable(),
source -> io.reactivex.Flowable.fromPublisher(source)
.toObservable().ignoreElements()
io.reactivex.Completable::fromPublisher
);
}
}
@@ -304,30 +301,26 @@ public class ReactiveAdapterRegistry {
io.reactivex.rxjava3.core.Observable::empty),
source -> ((io.reactivex.rxjava3.core.Observable<?>) source).toFlowable(
io.reactivex.rxjava3.core.BackpressureStrategy.BUFFER),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable()
io.reactivex.rxjava3.core.Observable::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleRequiredValue(io.reactivex.rxjava3.core.Single.class),
source -> ((io.reactivex.rxjava3.core.Single<?>) source).toFlowable(),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable().singleElement().toSingle()
io.reactivex.rxjava3.core.Single::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.singleOptionalValue(
io.reactivex.rxjava3.core.Maybe.class,
io.reactivex.rxjava3.core.Maybe::empty),
source -> ((io.reactivex.rxjava3.core.Maybe<?>) source).toFlowable(),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable().singleElement()
io.reactivex.rxjava3.core.Maybe::fromPublisher
);
registry.registerReactiveType(
ReactiveTypeDescriptor.noValue(
io.reactivex.rxjava3.core.Completable.class,
io.reactivex.rxjava3.core.Completable::complete),
source -> ((io.reactivex.rxjava3.core.Completable) source).toFlowable(),
source -> io.reactivex.rxjava3.core.Flowable.fromPublisher(source)
.toObservable().ignoreElements()
io.reactivex.rxjava3.core.Completable::fromPublisher
);
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,8 +18,8 @@ package org.springframework.core.annotation;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntFunction;
@@ -31,10 +31,11 @@ import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
/**
* Collector implementations that provide various reduction operations for
* {@link Collector} implementations that provide various reduction operations for
* {@link MergedAnnotation} instances.
*
* @author Phillip Webb
* @author Sam Brannen
* @since 5.2
*/
public abstract class MergedAnnotationCollectors {
@@ -52,13 +53,16 @@ public abstract class MergedAnnotationCollectors {
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link LinkedHashSet} containing {@linkplain MergedAnnotation#synthesize()
* synthesized} versions.
* <p>The collector returned by this method is effectively equivalent to
* {@code Collectors.mapping(MergedAnnotation::synthesize, Collectors.toCollection(LinkedHashSet::new))}
* but avoids the creation of a composite collector.
* @param <A> the annotation type
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link Set}
*/
public static <A extends Annotation> Collector<MergedAnnotation<A>, ?, Set<A>> toAnnotationSet() {
return Collector.of(ArrayList<A>::new, (list, annotation) -> list.add(annotation.synthesize()),
MergedAnnotationCollectors::addAll, LinkedHashSet::new);
return Collector.of(LinkedHashSet::new, (set, annotation) -> set.add(annotation.synthesize()),
MergedAnnotationCollectors::combiner);
}
/**
@@ -90,14 +94,14 @@ public abstract class MergedAnnotationCollectors {
IntFunction<R[]> generator) {
return Collector.of(ArrayList::new, (list, annotation) -> list.add(annotation.synthesize()),
MergedAnnotationCollectors::addAll, list -> list.toArray(generator.apply(list.size())));
MergedAnnotationCollectors::combiner, list -> list.toArray(generator.apply(list.size())));
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param adaptations the adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
@@ -111,13 +115,13 @@ public abstract class MergedAnnotationCollectors {
}
/**
* Create a new {@link Collector} that accumulates merged annotations to an
* Create a new {@link Collector} that accumulates merged annotations to a
* {@link MultiValueMap} with items {@linkplain MultiValueMap#add(Object, Object)
* added} from each merged annotation
* {@link MergedAnnotation#asMap(Adapt...) as a map}.
* {@linkplain MergedAnnotation#asMap(Adapt...) as a map}.
* @param <A> the annotation type
* @param adaptations the adaptations that should be applied to the annotation values
* @param finisher the finisher function for the new {@link MultiValueMap}
* @param adaptations the adaptations that should be applied to the annotation values
* @return a {@link Collector} which collects and synthesizes the
* annotations into a {@link LinkedMultiValueMap}
* @see #toMultiValueMap(MergedAnnotation.Adapt...)
@@ -130,7 +134,7 @@ public abstract class MergedAnnotationCollectors {
IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS);
return Collector.of(LinkedMultiValueMap::new,
(map, annotation) -> annotation.asMap(adaptations).forEach(map::add),
MergedAnnotationCollectors::merge, finisher, characteristics);
MergedAnnotationCollectors::combiner, finisher, characteristics);
}
@@ -138,13 +142,22 @@ public abstract class MergedAnnotationCollectors {
return instance == candidate;
}
private static <E, L extends List<E>> L addAll(L list, L additions) {
list.addAll(additions);
return list;
/**
* {@link Collector#combiner() Combiner} for collections.
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
*/
private static <E, C extends Collection<E>> C combiner(C collection, C additions) {
collection.addAll(additions);
return collection;
}
private static <K, V> MultiValueMap<K, V> merge(MultiValueMap<K, V> map,
MultiValueMap<K, V> additions) {
/**
* {@link Collector#combiner() Combiner} for multi-value maps.
* <p>This method is only invoked if the {@link java.util.stream.Stream} is
* processed in {@linkplain java.util.stream.Stream#parallel() parallel}.
*/
private static <K, V> MultiValueMap<K, V> combiner(MultiValueMap<K, V> map, MultiValueMap<K, V> additions) {
map.addAll(additions);
return map;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -118,7 +118,7 @@ public final class Property {
}
// package private
// Package private
MethodParameter getMethodParameter() {
return this.methodParameter;
@@ -132,7 +132,7 @@ public final class Property {
}
// internal helpers
// Internal helpers
private String resolveName() {
if (this.readMethod != null) {
@@ -142,10 +142,13 @@ public final class Property {
}
else {
index = this.readMethod.getName().indexOf("is");
if (index == -1) {
throw new IllegalArgumentException("Not a getter method");
if (index != -1) {
index += 2;
}
else {
// Record-style plain accessor method, e.g. name()
index = 0;
}
index += 2;
}
return StringUtils.uncapitalize(this.readMethod.getName().substring(index));
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -63,34 +63,36 @@ import org.springframework.util.StringUtils;
* <p>Individual expressions can be compiled by calling {@code SpelCompiler.compile(expression)}.
*
* @author Andy Clement
* @author Juergen Hoeller
* @since 4.1
*/
public final class SpelCompiler implements Opcodes {
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
private static final int CLASSES_DEFINED_LIMIT = 100;
private static final Log logger = LogFactory.getLog(SpelCompiler.class);
// A compiler is created for each classloader, it manages a child class loader of that
// classloader and the child is used to load the compiled expressions.
private static final Map<ClassLoader, SpelCompiler> compilers = new ConcurrentReferenceHashMap<>();
// The child ClassLoader used to load the compiled expression classes
private ChildClassLoader ccl;
private volatile ChildClassLoader childClassLoader;
// Counter suffix for generated classes within this SpelCompiler instance
private final AtomicInteger suffixId = new AtomicInteger(1);
private SpelCompiler(@Nullable ClassLoader classloader) {
this.ccl = new ChildClassLoader(classloader);
this.childClassLoader = new ChildClassLoader(classloader);
}
/**
* Attempt compilation of the supplied expression. A check is made to see
* if it is compilable before compilation proceeds. The check involves
* visiting all the nodes in the expression Ast and ensuring enough state
* visiting all the nodes in the expression AST and ensuring enough state
* is known about them that bytecode can be generated for them.
* @param expression the expression to compile
* @return an instance of the class implementing the compiled expression,
@@ -125,7 +127,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Generate the class that encapsulates the compiled expression and define it.
* The generated class will be a subtype of CompiledExpression.
* The generated class will be a subtype of CompiledExpression.
* @param expressionToCompile the expression to be compiled
* @return the expression call, or {@code null} if the decision was to opt out of
* compilation during code generation
@@ -135,7 +137,7 @@ public final class SpelCompiler implements Opcodes {
// Create class outline 'spel/ExNNN extends org.springframework.expression.spel.CompiledExpression'
String className = "spel/Ex" + getNextSuffix();
ClassWriter cw = new ExpressionClassWriter();
cw.visit(V1_5, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null);
// Create default constructor
MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "<init>", "()V", null, null);
@@ -150,7 +152,7 @@ public final class SpelCompiler implements Opcodes {
// Create getValue() method
mv = cw.visitMethod(ACC_PUBLIC, "getValue",
"(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;", null,
new String[ ]{"org/springframework/expression/EvaluationException"});
new String[] {"org/springframework/expression/EvaluationException"});
mv.visitCode();
CodeFlow cf = new CodeFlow(className, cw);
@@ -187,7 +189,7 @@ public final class SpelCompiler implements Opcodes {
/**
* Load a compiled expression class. Makes sure the classloaders aren't used too much
* because they anchor compiled classes in memory and prevent GC. If you have expressions
* because they anchor compiled classes in memory and prevent GC. If you have expressions
* continually recompiling over time then by replacing the classloader periodically
* at least some of the older variants can be garbage collected.
* @param name the name of the class
@@ -196,12 +198,25 @@ public final class SpelCompiler implements Opcodes {
*/
@SuppressWarnings("unchecked")
private Class<? extends CompiledExpression> loadClass(String name, byte[] bytes) {
if (this.ccl.getClassesDefinedCount() > CLASSES_DEFINED_LIMIT) {
this.ccl = new ChildClassLoader(this.ccl.getParent());
ChildClassLoader ccl = this.childClassLoader;
if (ccl.getClassesDefinedCount() >= CLASSES_DEFINED_LIMIT) {
synchronized (this) {
ChildClassLoader currentCcl = this.childClassLoader;
if (ccl == currentCcl) {
// Still the same ClassLoader that needs to be replaced...
ccl = new ChildClassLoader(ccl.getParent());
this.childClassLoader = ccl;
}
else {
// Already replaced by some other thread, let's pick it up.
ccl = currentCcl;
}
}
}
return (Class<? extends CompiledExpression>) this.ccl.defineClass(name, bytes);
return (Class<? extends CompiledExpression>) ccl.defineClass(name, bytes);
}
/**
* Factory method for compiler instances. The returned SpelCompiler will
* attach a class loader as the child of the given class loader and this
@@ -211,21 +226,28 @@ public final class SpelCompiler implements Opcodes {
*/
public static SpelCompiler getCompiler(@Nullable ClassLoader classLoader) {
ClassLoader clToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader());
synchronized (compilers) {
SpelCompiler compiler = compilers.get(clToUse);
if (compiler == null) {
compiler = new SpelCompiler(clToUse);
compilers.put(clToUse, compiler);
// Quick check for existing compiler without lock contention
SpelCompiler compiler = compilers.get(clToUse);
if (compiler == null) {
// Full lock now since we're creating a child ClassLoader
synchronized (compilers) {
compiler = compilers.get(clToUse);
if (compiler == null) {
compiler = new SpelCompiler(clToUse);
compilers.put(clToUse, compiler);
}
}
return compiler;
}
return compiler;
}
/**
* Request that an attempt is made to compile the specified expression. It may fail if
* components of the expression are not suitable for compilation or the data types
* involved are not suitable for compilation. Used for testing.
* @return true if the expression was successfully compiled
* Request that an attempt is made to compile the specified expression.
* It may fail if components of the expression are not suitable for compilation
* or the data types involved are not suitable for compilation. Used for testing.
* @param expression the expression to compile
* @return {@code true} if the expression was successfully compiled,
* {@code false} otherwise
*/
public static boolean compile(Expression expression) {
return (expression instanceof SpelExpression && ((SpelExpression) expression).compileExpression());
@@ -250,24 +272,27 @@ public final class SpelCompiler implements Opcodes {
private static final URL[] NO_URLS = new URL[0];
private int classesDefinedCount = 0;
private final AtomicInteger classesDefinedCount = new AtomicInteger(0);
public ChildClassLoader(@Nullable ClassLoader classLoader) {
super(NO_URLS, classLoader);
}
int getClassesDefinedCount() {
return this.classesDefinedCount;
}
public Class<?> defineClass(String name, byte[] bytes) {
Class<?> clazz = super.defineClass(name, bytes, 0, bytes.length);
this.classesDefinedCount++;
this.classesDefinedCount.incrementAndGet();
return clazz;
}
public int getClassesDefinedCount() {
return this.classesDefinedCount.get();
}
}
/**
* An ASM ClassWriter extension bound to the SpelCompiler's ClassLoader.
*/
private class ExpressionClassWriter extends ClassWriter {
public ExpressionClassWriter() {
@@ -276,7 +301,7 @@ public final class SpelCompiler implements Opcodes {
@Override
protected ClassLoader getClassLoader() {
return ccl;
return childClassLoader;
}
}
@@ -395,6 +395,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
if (method == null) {
method = findMethodForProperty(getPropertyMethodSuffixes(propertyName),
"is", clazz, mustBeStatic, 0, BOOLEAN_TYPES);
if (method == null) {
// Record-style plain accessor method, e.g. name()
method = findMethodForProperty(new String[] {propertyName},
"", clazz, mustBeStatic, 0, ANY_TYPES);
}
}
return method;
}
@@ -683,12 +688,11 @@ public class ReflectivePropertyAccessor implements PropertyAccessor {
return true;
}
getterName = "is" + StringUtils.capitalize(name);
return getterName.equals(method.getName());
}
else {
Field field = (Field) this.member;
return field.getName().equals(name);
if (getterName.equals(method.getName())) {
return true;
}
}
return this.member.getName().equals(name);
}
@Override
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,6 +36,7 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.testresources.Inventor;
import org.springframework.expression.spel.testresources.Person;
import org.springframework.expression.spel.testresources.RecordPerson;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
@@ -191,6 +192,20 @@ public class PropertyAccessTests extends AbstractExpressionTests {
parser.parseExpression("name='p3'").getValue(context, target));
}
@Test
public void propertyReadOnlyWithRecordStyle() {
EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build();
Expression expr = parser.parseExpression("name");
RecordPerson target1 = new RecordPerson("p1");
assertThat(expr.getValue(context, target1)).isEqualTo("p1");
RecordPerson target2 = new RecordPerson("p2");
assertThat(expr.getValue(context, target2)).isEqualTo("p2");
assertThatExceptionOfType(SpelEvaluationException.class).isThrownBy(() ->
parser.parseExpression("name='p3'").getValue(context, target2));
}
@Test
public void propertyReadWrite() {
EvaluationContext context = SimpleEvaluationContext.forReadWriteDataBinding().build();
@@ -4180,6 +4180,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(expression.getValue(tc)).isEqualTo("value4");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value4");
// record-style accessor
expression = parser.parseExpression("strawberry");
assertCantCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value5");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value5");
}
@Test
@@ -4553,23 +4560,9 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
Object v = expression.getValue(ctx,holder);
assertThat(v).isEqualTo("abc");
// // time it interpreted
// long stime = System.currentTimeMillis();
// for (int i = 0; i < 100000; i++) {
// v = expression.getValue(ctx,holder);
// }
// System.out.println((System.currentTimeMillis() - stime));
assertCanCompile(expression);
v = expression.getValue(ctx,holder);
assertThat(v).isEqualTo("abc");
// // time it compiled
// stime = System.currentTimeMillis();
// for (int i = 0; i < 100000; i++) {
// v = expression.getValue(ctx,holder);
// }
// System.out.println((System.currentTimeMillis() - stime));
}
@Test
@@ -4985,13 +4978,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(fast.compileExpression()).isTrue();
r.setValue2(null);
// try the numbers 0,1,2,null
for (int i=0;i<4;i++) {
r.setValue(i<3?i:null);
for (int i = 0; i < 4; i++) {
r.setValue(i < 3 ? i : null);
boolean slowResult = (Boolean)slow.getValue(ctx);
boolean fastResult = (Boolean)fast.getValue(ctx);
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
assertThat(fastResult).as(" Differing results: expression="+expressionText+
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
assertThat(fastResult).as("Differing results: expression=" + expressionText +
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
}
}
@@ -5002,13 +4994,12 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
assertThat(fast.compileExpression()).isTrue();
Reg r = (Reg)ctx.getRootObject().getValue();
// try the numbers 0,1,2,null
for (int i=0;i<4;i++) {
r.setValue(i<3?i:null);
for (int i = 0; i < 4; i++) {
r.setValue(i < 3 ? i : null);
boolean slowResult = (Boolean)slow.getValue(ctx);
boolean fastResult = (Boolean)fast.getValue(ctx);
// System.out.println("Trying "+expressionText+" with value="+r.getValue()+" result is "+slowResult);
assertThat(fastResult).as(" Differing results: expression="+expressionText+
" value="+r.getValue()+" slow="+slowResult+" fast="+fastResult).isEqualTo(slowResult);
assertThat(fastResult).as("Differing results: expression=" + expressionText +
" value=" + r.getValue() + " slow=" + slowResult + " fast="+fastResult).isEqualTo(slowResult);
}
}
@@ -5839,7 +5830,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
public String orange = "value1";
public static String apple = "value2";
public long peach = 34L;
public String getBanana() {
@@ -5849,6 +5839,10 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
public static String getPlum() {
return "value4";
}
public String strawberry() {
return "value5";
}
}
@@ -76,7 +76,6 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*/
public class SpelReproTests extends AbstractExpressionTests {
@Test
public void NPE_SPR5661() {
evaluate("joinThreeStrings('a',null,'c')", "anullc", String.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,9 +16,10 @@
package org.springframework.expression.spel.testresources;
///CLOVER:OFF
public class Person {
private String privateName;
Company company;
public Person(String name) {
@@ -41,4 +42,5 @@ public class Person {
public Company getCompany() {
return company;
}
}
@@ -0,0 +1,42 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.expression.spel.testresources;
public class RecordPerson {
private String name;
private Company company;
public RecordPerson(String name) {
this.name = name;
}
public RecordPerson(String name, Company company) {
this.name = name;
this.company = company;
}
public String name() {
return name;
}
public Company company() {
return company;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,19 +19,25 @@ package org.springframework.expression.spel.testresources;
import java.util.List;
public class TestAddress{
private String street;
private List<String> crossStreets;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public List<String> getCrossStreets() {
return crossStreets;
}
public void setCrossStreets(List<String> crossStreets) {
this.crossStreets = crossStreets;
}
private String street;
private List<String> crossStreets;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
public List<String> getCrossStreets() {
return crossStreets;
}
public void setCrossStreets(List<String> crossStreets) {
this.crossStreets = crossStreets;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,19 +17,25 @@
package org.springframework.expression.spel.testresources;
public class TestPerson {
private String name;
private TestAddress address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestAddress getAddress() {
return address;
}
public void setAddress(TestAddress address) {
this.address = address;
}
private String name;
private TestAddress address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public TestAddress getAddress() {
return address;
}
public void setAddress(TestAddress address) {
this.address = address;
}
}
@@ -637,21 +637,23 @@ public class CallMetaDataContext {
schemaNameToUse = this.metaDataProvider.schemaNameToUse(getSchemaName());
}
String procedureNameToUse = this.metaDataProvider.procedureNameToUse(getProcedureName());
if (isFunction() || isReturnValueRequired()) {
callString = new StringBuilder().append("{? = call ").
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
append(procedureNameToUse).append("(");
callString = new StringBuilder("{? = call ");
parameterCount = -1;
}
else {
callString = new StringBuilder().append("{call ").
append(StringUtils.hasLength(catalogNameToUse) ? catalogNameToUse + "." : "").
append(StringUtils.hasLength(schemaNameToUse) ? schemaNameToUse + "." : "").
append(procedureNameToUse).append("(");
callString = new StringBuilder("{call ");
}
if (StringUtils.hasLength(catalogNameToUse)) {
callString.append(catalogNameToUse).append(".");
}
if (StringUtils.hasLength(schemaNameToUse)) {
callString.append(schemaNameToUse).append(".");
}
callString.append(this.metaDataProvider.procedureNameToUse(getProcedureName()));
callString.append("(");
for (SqlParameter parameter : this.callParameters) {
if (!parameter.isResultsParameter()) {
if (parameterCount > 0) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -83,7 +83,7 @@ public abstract class NamedParameterUtils {
Assert.notNull(sql, "SQL must not be null");
Set<String> namedParameters = new HashSet<>();
String sqlToUse = sql;
StringBuilder sqlToUse = new StringBuilder(sql);
List<ParameterHolder> parameterList = new ArrayList<>();
char[] statement = sql.toCharArray();
@@ -155,7 +155,7 @@ public abstract class NamedParameterUtils {
int j = i + 1;
if (j < statement.length && statement[j] == ':') {
// escaped ":" should be skipped
sqlToUse = sqlToUse.substring(0, i - escapes) + sqlToUse.substring(i - escapes + 1);
sqlToUse.deleteCharAt(i - escapes);
escapes++;
i = i + 2;
continue;
@@ -174,7 +174,7 @@ public abstract class NamedParameterUtils {
}
i++;
}
ParsedSql parsedSql = new ParsedSql(sqlToUse);
ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
for (ParameterHolder ph : parameterList) {
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
}
@@ -56,6 +56,9 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
@Nullable
private TcpOperations<byte[]> tcpClient;
@Nullable
private TaskScheduler taskScheduler;
private boolean autoStartup = true;
@Nullable
@@ -64,8 +67,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
@Nullable
private String userRegistryBroadcast;
@Nullable
private TaskScheduler taskScheduler;
public StompBrokerRelayRegistration(SubscribableChannel clientInboundChannel,
MessageChannel clientOutboundChannel, String[] destinationPrefixes) {
@@ -181,8 +182,27 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
* specified are effectively ignored.
* @since 4.3.15
*/
public void setTcpClient(TcpOperations<byte[]> tcpClient) {
public StompBrokerRelayRegistration setTcpClient(TcpOperations<byte[]> tcpClient) {
this.tcpClient = tcpClient;
return this;
}
/**
* Some STOMP clients (e.g. stomp-js) always send heartbeats at a fixed rate
* but others (Spring STOMP client) do so only when no other messages are
* sent. However messages with a non-broker {@link #getDestinationPrefixes()
* destination prefix} aren't forwarded and as a result the broker may deem
* the connection inactive.
* <p>When this {@link TaskScheduler} is set, it is used to reset a count of
* the number of messages sent from client to broker since the beginning of
* the current heartbeat period. This is then used to decide whether to send
* a heartbeat to the broker when ignoring a message with a non-broker
* destination prefix.
* @since 5.3
*/
public StompBrokerRelayRegistration setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
return this;
}
/**
@@ -228,26 +248,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
return this;
}
/**
* Some STOMP clients (e.g. stomp-js) always send heartbeats at a fixed rate
* but others (Spring STOMP client) do so only when no other messages are
* sent. However messages with a non-broker {@link #getDestinationPrefixes()
* destination prefix} aren't forwarded and as a result the broker may deem
* the connection inactive.
*
* <p>When this {@link TaskScheduler} is set, it is used to reset a count of
* the number of messages sent from client to broker since the beginning of
* the current heartbeat period. This is then used to decide whether to send
* a heartbeat to the broker when ignoring a message with a non-broker
* destination prefix.
*
* @param taskScheduler the scheduler to use
* @since 5.3
*/
public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) {
this.taskScheduler = taskScheduler;
}
@Nullable
protected String getUserRegistryBroadcast() {
return this.userRegistryBroadcast;
@@ -256,7 +256,6 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration {
@Override
protected StompBrokerRelayMessageHandler getMessageHandler(SubscribableChannel brokerChannel) {
StompBrokerRelayMessageHandler handler = new StompBrokerRelayMessageHandler(
getClientInboundChannel(), getClientOutboundChannel(),
brokerChannel, getDestinationPrefixes());
@@ -95,7 +95,7 @@ abstract class NamedParameterUtils {
Assert.notNull(sql, "SQL must not be null");
Set<String> namedParameters = new HashSet<>();
String sqlToUse = sql;
StringBuilder sqlToUse = new StringBuilder(sql);
List<ParameterHolder> parameterList = new ArrayList<>();
char[] statement = sql.toCharArray();
@@ -171,8 +171,7 @@ abstract class NamedParameterUtils {
int j = i + 1;
if (j < statement.length && statement[j] == ':') {
// escaped ":" should be skipped
sqlToUse = sqlToUse.substring(0, i - escapes)
+ sqlToUse.substring(i - escapes + 1);
sqlToUse.deleteCharAt(i - escapes);
escapes++;
i = i + 2;
continue;
@@ -181,7 +180,7 @@ abstract class NamedParameterUtils {
}
i++;
}
ParsedSql parsedSql = new ParsedSql(sqlToUse);
ParsedSql parsedSql = new ParsedSql(sqlToUse.toString());
for (ParameterHolder ph : parameterList) {
parsedSql.addNamedParameter(ph.getParameterName(), ph.getStartIndex(), ph.getEndIndex());
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -83,7 +83,7 @@ public @interface ActiveProfiles {
* <p>The default value is {@code true}, which means that a test
* class will <em>inherit</em> bean definition profiles defined by a
* test superclass. Specifically, the bean definition profiles for a test
* class will be added to the list of bean definition profiles
* class will be appended to the list of bean definition profiles
* defined by a test superclass. Thus, subclasses have the option of
* <em>extending</em> the list of bean definition profiles.
* <p>If {@code inheritProfiles} is set to {@code false}, the bean
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,8 +19,8 @@ package org.springframework.test.context;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.TreeSet;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
@@ -533,8 +533,8 @@ public class MergedContextConfiguration implements Serializable {
return EMPTY_STRING_ARRAY;
}
// Active profiles must be unique and sorted
Set<String> profilesSet = new TreeSet<>(Arrays.asList(activeProfiles));
// Active profiles must be unique
Set<String> profilesSet = new LinkedHashSet<>(Arrays.asList(activeProfiles));
return StringUtils.toStringArray(profilesSet);
}
@@ -492,10 +492,11 @@ public abstract class TestContextAnnotationUtils {
Assert.notNull(annotation, "Annotation must not be null");
this.rootDeclaringClass = rootDeclaringClass;
this.declaringClass = declaringClass;
this.annotation = (T) AnnotatedElementUtils.findMergedAnnotation(
T mergedAnnotation = (T) AnnotatedElementUtils.findMergedAnnotation(
rootDeclaringClass, annotation.annotationType());
Assert.state(this.annotation != null,
Assert.state(mergedAnnotation != null,
() -> "Failed to find merged annotation for " + annotation);
this.annotation = mergedAnnotation;
}
public Class<?> getRootDeclaringClass() {
@@ -545,15 +546,13 @@ public abstract class TestContextAnnotationUtils {
/**
* Find <strong>all</strong> annotations of the specified annotation type
* that are present or meta-present on the {@linkplain #getRootDeclaringClass()
* root declaring class} of this descriptor.
* root declaring class} of this descriptor or on any interfaces that the
* root declaring class implements.
* @return the set of all merged, synthesized {@code Annotations} found,
* or an empty set if none were found
*/
public Set<T> findAllLocalMergedAnnotations() {
SearchStrategy searchStrategy =
(getEnclosingConfiguration(getRootDeclaringClass()) == EnclosingConfiguration.INHERIT ?
SearchStrategy.TYPE_HIERARCHY_AND_ENCLOSING_CLASSES :
SearchStrategy.TYPE_HIERARCHY);
SearchStrategy searchStrategy = SearchStrategy.TYPE_HIERARCHY;
return MergedAnnotations.from(getRootDeclaringClass(), searchStrategy, RepeatableContainers.none())
.stream(getAnnotationType())
.filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex))
@@ -16,8 +16,10 @@
package org.springframework.test.context.support;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -70,8 +72,8 @@ abstract class ActiveProfilesUtils {
static String[] resolveActiveProfiles(Class<?> testClass) {
Assert.notNull(testClass, "Class must not be null");
Set<String> activeProfiles = new TreeSet<>();
AnnotationDescriptor<ActiveProfiles> descriptor = findAnnotationDescriptor(testClass, ActiveProfiles.class);
List<String[]> profileArrays = new ArrayList<>();
if (descriptor == null && logger.isDebugEnabled()) {
logger.debug(String.format(
@@ -107,16 +109,24 @@ abstract class ActiveProfilesUtils {
String[] profiles = resolver.resolve(rootDeclaringClass);
if (!ObjectUtils.isEmpty(profiles)) {
for (String profile : profiles) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}
}
// Prepend to the list so that we can later traverse "down" the hierarchy
// to ensure that we retain the top-down profile registration order
// within a test class hierarchy.
profileArrays.add(0, profiles);
}
descriptor = (annotation.inheritProfiles() ? descriptor.next() : null);
}
Set<String> activeProfiles = new LinkedHashSet<>();
for (String[] profiles : profileArrays) {
for (String profile : profiles) {
if (StringUtils.hasText(profile)) {
activeProfiles.add(profile.trim());
}
}
}
return StringUtils.toStringArray(activeProfiles);
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -143,7 +143,7 @@ class MergedContextConfigurationTests {
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles1, loader);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(),
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles2, loader);
assertThat(mergedConfig2).hasSameHashCodeAs(mergedConfig1);
assertThat(mergedConfig2.hashCode()).isNotEqualTo(mergedConfig1.hashCode());
}
@Test
@@ -339,13 +339,13 @@ class MergedContextConfigurationTests {
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles1, loader);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(),
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles2, loader);
assertThat(mergedConfig2).isEqualTo(mergedConfig1);
assertThat(mergedConfig2).isNotEqualTo(mergedConfig1);
}
@Test
void equalsWithSameDuplicateProfiles() {
String[] activeProfiles1 = new String[] { "catbert", "dogbert" };
String[] activeProfiles2 = new String[] { "dogbert", "catbert", "dogbert", "catbert" };
String[] activeProfiles2 = new String[] { "catbert", "dogbert", "catbert", "dogbert", "catbert" };
MergedContextConfiguration mergedConfig1 = new MergedContextConfiguration(getClass(),
EMPTY_STRING_ARRAY, EMPTY_CLASS_ARRAY, activeProfiles1, loader);
MergedContextConfiguration mergedConfig2 = new MergedContextConfiguration(getClass(),
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -90,8 +90,8 @@ class ContextCacheTests {
int size = 0, hit = 0, miss = 0;
loadCtxAndAssertStats(FooBarProfilesTestCase.class, ++size, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
// Profiles {foo, bar} MUST hash to the same as {bar, foo}
loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, ++hit, miss);
// Profiles {foo, bar} should not hash to the same as {bar,foo}
loadCtxAndAssertStats(BarFooProfilesTestCase.class, ++size, hit, ++miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(FooBarProfilesTestCase.class, size, ++hit, miss);
loadCtxAndAssertStats(BarFooProfilesTestCase.class, size, ++hit, miss);
@@ -67,12 +67,12 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
@Test
void resolveActiveProfilesWithDuplicatedProfiles() {
assertResolvedProfiles(DuplicatedProfiles.class, "bar", "baz", "foo");
assertResolvedProfiles(DuplicatedProfiles.class, "foo", "bar", "baz");
}
@Test
void resolveActiveProfilesWithLocalAndInheritedDuplicatedProfiles() {
assertResolvedProfiles(ExtendedDuplicatedProfiles.class, "bar", "baz", "cat", "dog", "foo");
assertResolvedProfiles(ExtendedDuplicatedProfiles.class, "foo", "bar", "baz", "cat", "dog");
}
@Test
@@ -92,12 +92,12 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
@Test
void resolveActiveProfilesWithLocalAndInheritedAnnotations() {
assertResolvedProfiles(LocationsBar.class, "bar", "foo");
assertResolvedProfiles(LocationsBar.class, "foo", "bar");
}
@Test
void resolveActiveProfilesWithOverriddenAnnotation() {
assertResolvedProfiles(Animals.class, "cat", "dog");
assertResolvedProfiles(Animals.class, "dog", "cat");
}
/**
@@ -129,7 +129,7 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
*/
@Test
void resolveActiveProfilesWithLocalAndInheritedMetaAnnotations() {
assertResolvedProfiles(MetaLocationsBar.class, "bar", "foo");
assertResolvedProfiles(MetaLocationsBar.class, "foo", "bar");
}
/**
@@ -137,7 +137,7 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
*/
@Test
void resolveActiveProfilesWithOverriddenMetaAnnotation() {
assertResolvedProfiles(MetaAnimals.class, "cat", "dog");
assertResolvedProfiles(MetaAnimals.class, "dog", "cat");
}
/**
@@ -161,7 +161,7 @@ class ActiveProfilesUtilsTests extends AbstractContextConfigurationUtilsTests {
*/
@Test
void resolveActiveProfilesWithMergedInheritedResolver() {
assertResolvedProfiles(MergedInheritedFooActiveProfilesResolverTestCase.class, "bar", "foo");
assertResolvedProfiles(MergedInheritedFooActiveProfilesResolverTestCase.class, "foo", "bar");
}
/**
@@ -373,6 +373,8 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
void buildMergedConfigWithDuplicateConfigurationOnEnclosingClassAndNestedClass() {
compareApplesToApples(AppleConfigTestCase.class, AppleConfigTestCase.Nested.class);
compareApplesToApples(AppleConfigTestCase.Nested.class, AppleConfigTestCase.Nested.DoubleNested.class);
compareApplesToOranges(ApplesAndOrangesConfigTestCase.class, ApplesAndOrangesConfigTestCase.Nested.class);
compareApplesToOranges(ApplesAndOrangesConfigTestCase.Nested.class, ApplesAndOrangesConfigTestCase.Nested.DoubleNested.class);
}
private void compareApplesToApples(Class<?> parent, Class<?> child) {
@@ -400,7 +402,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
DelegatingSmartContextLoader.class);
assertThat(parentMergedConfig.getActiveProfiles()).as("active profiles")
.containsExactly("apples", "oranges")
.containsExactly("oranges", "apples")
.isEqualTo(childMergedConfig.getActiveProfiles());
assertThat(parentMergedConfig).isEqualTo(childMergedConfig);
}
@@ -531,7 +533,7 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
}
@ContextConfiguration(classes = AppleConfig.class)
@ActiveProfiles({"apples", "oranges"})
@ActiveProfiles({"oranges", "apples"})
static class ApplesAndOrangesConfigTestCase {
@ContextConfiguration(classes = AppleConfig.class)
@@ -539,19 +541,19 @@ class BootstrapTestUtilsMergedConfigTests extends AbstractContextConfigurationUt
class Nested {
@ContextConfiguration(classes = AppleConfig.class)
@ActiveProfiles(profiles = {"apples", "oranges", "apples"}, inheritProfiles = false)
@ActiveProfiles(profiles = {"oranges", "apples", "oranges"}, inheritProfiles = false)
class DoubleNested {
}
}
}
@ContextConfiguration(classes = AppleConfig.class)
@ActiveProfiles(profiles = {"oranges", "apples"}, inheritProfiles = false)
@ActiveProfiles(profiles = {"oranges", "apples", "oranges"}, inheritProfiles = false)
static class DuplicateConfigApplesAndOrangesConfigTestCase extends ApplesAndOrangesConfigTestCase {
}
@ContextConfiguration(classes = AppleConfig.class)
@ActiveProfiles(profiles = {"apples", "oranges", "apples"}, inheritProfiles = false)
@ActiveProfiles(profiles = {"oranges", "apples", "oranges"}, inheritProfiles = false)
static class SubDuplicateConfigApplesAndOrangesConfigTestCase extends DuplicateConfigApplesAndOrangesConfigTestCase {
}
@@ -352,9 +352,9 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
}
return new ReactiveTransactionSupport(adapter);
});
Publisher<?> publisher = (Publisher<?>) txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
return (isSuspendingFunction ? (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow(publisher) :
KotlinDelegate.awaitSingleOrNull(publisher, ((CoroutinesInvocationCallback) invocation).getContinuation())) : publisher);
Object result = txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, (ReactiveTransactionManager) tm);
return (isSuspendingFunction ? (hasSuspendingFlowReturnType ? KotlinDelegate.asFlow((Publisher<?>) result) :
KotlinDelegate.awaitSingleOrNull((Publisher<?>) result, ((CoroutinesInvocationCallback) invocation).getContinuation())) : result);
}
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
@@ -875,8 +875,8 @@ public abstract class TransactionAspectSupport implements BeanFactoryAware, Init
String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// Optimize for Mono
if (Mono.class.isAssignableFrom(method.getReturnType())) {
// For Mono and suspending functions not returning kotlinx.coroutines.flow.Flow
if (Mono.class.isAssignableFrom(method.getReturnType()) || (KotlinDetector.isSuspendingFunction(method) && !COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName()))) {
return TransactionContextManager.currentContext().flatMap(context ->
createTransactionIfNecessary(rtm, txAttr, joinpointIdentification).flatMap(it -> {
try {
@@ -0,0 +1,170 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.transaction.annotation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.runBlocking
import org.assertj.core.api.Assertions
import org.junit.jupiter.api.Test
import org.springframework.aop.framework.ProxyFactory
import org.springframework.transaction.interceptor.TransactionInterceptor
import org.springframework.transaction.testfixture.ReactiveCallCountingTransactionManager
/**
* @author Sebastien Deleuze
*/
class CoroutinesAnnotationTransactionInterceptorTests {
private val rtm = ReactiveCallCountingTransactionManager()
private val source = AnnotationTransactionAttributeSource()
@Test
fun suspendingNoValueSuccess() {
val proxyFactory = ProxyFactory()
proxyFactory.setTarget(TestWithCoroutines())
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
val proxy = proxyFactory.proxy as TestWithCoroutines
runBlocking {
proxy.suspendingNoValueSuccess()
}
assertReactiveGetTransactionAndCommitCount(1)
}
@Test
fun suspendingNoValueFailure() {
val proxyFactory = ProxyFactory()
proxyFactory.setTarget(TestWithCoroutines())
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
val proxy = proxyFactory.proxy as TestWithCoroutines
runBlocking {
try {
proxy.suspendingNoValueFailure()
}
catch (ex: IllegalStateException) {
}
}
assertReactiveGetTransactionAndRollbackCount(1)
}
@Test
fun suspendingValueSuccess() {
val proxyFactory = ProxyFactory()
proxyFactory.setTarget(TestWithCoroutines())
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
val proxy = proxyFactory.proxy as TestWithCoroutines
runBlocking {
Assertions.assertThat(proxy.suspendingValueSuccess()).isEqualTo("foo")
}
assertReactiveGetTransactionAndCommitCount(1)
}
@Test
fun suspendingValueFailure() {
val proxyFactory = ProxyFactory()
proxyFactory.setTarget(TestWithCoroutines())
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
val proxy = proxyFactory.proxy as TestWithCoroutines
runBlocking {
try {
proxy.suspendingValueFailure()
Assertions.fail("No exception thrown as expected")
}
catch (ex: IllegalStateException) {
}
}
assertReactiveGetTransactionAndRollbackCount(1)
}
@Test
fun suspendingFlowSuccess() {
val proxyFactory = ProxyFactory()
proxyFactory.setTarget(TestWithCoroutines())
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
val proxy = proxyFactory.proxy as TestWithCoroutines
runBlocking {
Assertions.assertThat(proxy.suspendingFlowSuccess().toList()).containsExactly("foo", "foo")
}
assertReactiveGetTransactionAndCommitCount(1)
}
@Test
fun flowSuccess() {
val proxyFactory = ProxyFactory()
proxyFactory.setTarget(TestWithCoroutines())
proxyFactory.addAdvice(TransactionInterceptor(rtm, source))
val proxy = proxyFactory.proxy as TestWithCoroutines
runBlocking {
Assertions.assertThat(proxy.flowSuccess().toList()).containsExactly("foo", "foo")
}
assertReactiveGetTransactionAndCommitCount(1)
}
private fun assertReactiveGetTransactionAndCommitCount(expectedCount: Int) {
Assertions.assertThat(rtm.begun).isEqualTo(expectedCount)
Assertions.assertThat(rtm.commits).isEqualTo(expectedCount)
}
private fun assertReactiveGetTransactionAndRollbackCount(expectedCount: Int) {
Assertions.assertThat(rtm.begun).isEqualTo(expectedCount)
Assertions.assertThat(rtm.rollbacks).isEqualTo(expectedCount)
}
@Transactional
open class TestWithCoroutines {
open suspend fun suspendingNoValueSuccess() {
delay(10)
}
open suspend fun suspendingNoValueFailure() {
delay(10)
throw IllegalStateException()
}
open suspend fun suspendingValueSuccess(): String {
delay(10)
return "foo"
}
open suspend fun suspendingValueFailure(): String {
delay(10)
throw IllegalStateException()
}
open fun flowSuccess(): Flow<String> {
return flow {
emit("foo")
delay(10)
emit("foo")
}
}
open suspend fun suspendingFlowSuccess(): Flow<String> {
delay(10)
return flow {
emit("foo")
delay(10)
emit("foo")
}
}
}
}
@@ -21,6 +21,7 @@ import java.lang.annotation.Annotation;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
@@ -99,8 +100,20 @@ public abstract class AbstractJackson2Decoder extends Jackson2CodecSupport imple
public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) {
JavaType javaType = getObjectMapper().constructType(elementType.getType());
// Skip String: CharSequenceDecoder + "*/*" comes after
return (!CharSequence.class.isAssignableFrom(elementType.toClass()) &&
getObjectMapper().canDeserialize(javaType) && supportsMimeType(mimeType));
if (CharSequence.class.isAssignableFrom(elementType.toClass()) || !supportsMimeType(mimeType)) {
return false;
}
if (!logger.isDebugEnabled()) {
return getObjectMapper().canDeserialize(javaType);
}
else {
AtomicReference<Throwable> causeRef = new AtomicReference<>();
if (getObjectMapper().canDeserialize(javaType, causeRef)) {
return true;
}
logWarningIfNecessary(javaType, causeRef.get());
return false;
}
}
@Override
@@ -23,6 +23,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import com.fasterxml.jackson.core.JsonEncoding;
import com.fasterxml.jackson.core.JsonGenerator;
@@ -33,6 +34,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SequenceWriter;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.ser.FilterProvider;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -47,6 +49,7 @@ import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.log.LogFormatUtils;
import org.springframework.http.MediaType;
import org.springframework.http.codec.HttpMessageEncoder;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.lang.Nullable;
@@ -111,8 +114,23 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
return false;
}
}
return (Object.class == clazz ||
(!String.class.isAssignableFrom(elementType.resolve(clazz)) && getObjectMapper().canSerialize(clazz)));
if (String.class.isAssignableFrom(elementType.resolve(clazz))) {
return false;
}
if (Object.class == clazz) {
return true;
}
if (!logger.isDebugEnabled()) {
return getObjectMapper().canSerialize(clazz);
}
else {
AtomicReference<Throwable> causeRef = new AtomicReference<>();
if (getObjectMapper().canSerialize(clazz, causeRef)) {
return true;
}
logWarningIfNecessary(clazz, causeRef.get());
return false;
}
}
@Override
@@ -132,7 +150,7 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
byte[] separator = getStreamingMediaTypeSeparator(mimeType);
if (separator != null) { // streaming
try {
ObjectWriter writer = createObjectWriter(elementType, mimeType, hints);
ObjectWriter writer = createObjectWriter(elementType, mimeType, null, hints);
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
JsonEncoding encoding = getJsonEncoding(mimeType);
JsonGenerator generator = getObjectMapper().getFactory().createGenerator(byteBuilder, encoding);
@@ -170,7 +188,18 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
public DataBuffer encodeValue(Object value, DataBufferFactory bufferFactory,
ResolvableType valueType, @Nullable MimeType mimeType, @Nullable Map<String, Object> hints) {
ObjectWriter writer = createObjectWriter(valueType, mimeType, hints);
Class<?> jsonView = null;
FilterProvider filters = null;
if (value instanceof MappingJacksonValue) {
MappingJacksonValue container = (MappingJacksonValue) value;
value = container.getValue();
jsonView = container.getSerializationView();
filters = container.getFilters();
}
ObjectWriter writer = createObjectWriter(valueType, mimeType, jsonView, hints);
if (filters != null) {
writer = writer.with(filters);
}
ByteArrayBuilder byteBuilder = new ByteArrayBuilder(writer.getFactory()._getBufferRecycler());
try {
JsonEncoding encoding = getJsonEncoding(mimeType);
@@ -252,10 +281,12 @@ public abstract class AbstractJackson2Encoder extends Jackson2CodecSupport imple
}
private ObjectWriter createObjectWriter(ResolvableType valueType, @Nullable MimeType mimeType,
@Nullable Map<String, Object> hints) {
@Nullable Class<?> jsonView, @Nullable Map<String, Object> hints) {
JavaType javaType = getJavaType(valueType.getType(), null);
Class<?> jsonView = (hints != null ? (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT) : null);
if (jsonView == null && hints != null) {
jsonView = (Class<?>) hints.get(Jackson2CodecSupport.JSON_VIEW_HINT);
}
ObjectWriter writer = (jsonView != null ?
getObjectMapper().writerWithView(jsonView) : getObjectMapper().writer());
@@ -26,6 +26,7 @@ import java.util.Map;
import com.fasterxml.jackson.annotation.JsonView;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.logging.Log;
@@ -108,7 +109,34 @@ public abstract class Jackson2CodecSupport {
protected boolean supportsMimeType(@Nullable MimeType mimeType) {
return (mimeType == null || this.mimeTypes.stream().anyMatch(m -> m.isCompatibleWith(mimeType)));
if (mimeType == null) {
return true;
}
for (MimeType supportedMimeType : this.mimeTypes) {
if (supportedMimeType.isCompatibleWith(mimeType)) {
return true;
}
}
return false;
}
/**
* Determine whether to log the given exception coming from a
* {@link ObjectMapper#canDeserialize} / {@link ObjectMapper#canSerialize} check.
* @param type the class that Jackson tested for (de-)serializability
* @param cause the Jackson-thrown exception to evaluate
* (typically a {@link JsonMappingException})
* @since 5.3.1
*/
protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) {
if (cause == null) {
return;
}
if (logger.isDebugEnabled()) {
String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") +
"serialization for type [" + type + "]";
logger.debug(msg, cause);
}
}
protected JavaType getJavaType(Type type, @Nullable Class<?> contextClass) {
@@ -18,6 +18,7 @@ package org.springframework.http.converter.json;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.lang.reflect.Type;
import java.nio.charset.Charset;
@@ -55,6 +56,7 @@ import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StreamUtils;
import org.springframework.util.TypeUtils;
/**
@@ -308,7 +310,8 @@ public abstract class AbstractJackson2HttpMessageConverter extends AbstractGener
MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputMessage.getBody(), encoding)) {
OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
try (JsonGenerator generator = this.objectMapper.getFactory().createGenerator(outputStream, encoding)) {
writePrefix(generator, object);
Object value = object;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -42,8 +42,8 @@ public class MapMethodProcessor implements HandlerMethodArgumentResolver, Handle
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Map.class.isAssignableFrom(parameter.getParameterType()) &&
parameter.getParameterAnnotations().length == 0;
return (Map.class.isAssignableFrom(parameter.getParameterType()) &&
parameter.getParameterAnnotations().length == 0);
}
@Override
@@ -70,8 +70,8 @@ public class MapMethodProcessor implements HandlerMethodArgumentResolver, Handle
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
throw new UnsupportedOperationException("Unexpected return type [" +
returnType.getParameterType().getName() + "] in method: " + returnType.getMethod());
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -70,8 +70,8 @@ public class ModelMethodProcessor implements HandlerMethodArgumentResolver, Hand
}
else {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
throw new UnsupportedOperationException("Unexpected return type [" +
returnType.getParameterType().getName() + "] in method: " + returnType.getMethod());
}
}
@@ -1023,15 +1023,21 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
if (this.path.length() == 0) {
return null;
}
String path = this.path.toString();
while (true) {
int index = path.indexOf("//");
if (index == -1) {
break;
String sanitized = getSanitizedPath(this.path);
return new HierarchicalUriComponents.FullPathComponent(sanitized);
}
private static String getSanitizedPath(final StringBuilder path) {
int index = path.indexOf("//");
if (index >= 0) {
StringBuilder sanitized = new StringBuilder(path);
while (index != -1) {
sanitized.deleteCharAt(index);
index = sanitized.indexOf("//", index);
}
path = path.substring(0, index) + path.substring(index + 1);
return sanitized.toString();
}
return new HierarchicalUriComponents.FullPathComponent(path);
return path.toString();
}
public void removeTrailingSlash() {
@@ -401,18 +401,17 @@ public class UrlPathHelper {
* <li>replace all "//" by "/"</li>
* </ul>
*/
private String getSanitizedPath(final String path) {
String sanitized = path;
while (true) {
int index = sanitized.indexOf("//");
if (index < 0) {
break;
}
else {
sanitized = sanitized.substring(0, index) + sanitized.substring(index + 1);
private static String getSanitizedPath(final String path) {
int index = path.indexOf("//");
if (index >= 0) {
StringBuilder sanitized = new StringBuilder(path);
while (index != -1) {
sanitized.deleteCharAt(index);
index = sanitized.indexOf("//", index);
}
return sanitized.toString();
}
return sanitized;
return path;
}
/**
@@ -612,15 +611,21 @@ public class UrlPathHelper {
removeSemicolonContentInternal(requestUri) : removeJsessionid(requestUri));
}
private String removeSemicolonContentInternal(String requestUri) {
private static String removeSemicolonContentInternal(String requestUri) {
int semicolonIndex = requestUri.indexOf(';');
while (semicolonIndex != -1) {
int slashIndex = requestUri.indexOf('/', semicolonIndex);
String start = requestUri.substring(0, semicolonIndex);
requestUri = (slashIndex != -1) ? start + requestUri.substring(slashIndex) : start;
semicolonIndex = requestUri.indexOf(';', semicolonIndex);
if (semicolonIndex == -1) {
return requestUri;
}
return requestUri;
StringBuilder sb = new StringBuilder(requestUri);
while (semicolonIndex != -1) {
int slashIndex = sb.indexOf("/", semicolonIndex + 1);
if (slashIndex == -1) {
return sb.substring(0, semicolonIndex);
}
sb.delete(semicolonIndex, slashIndex);
semicolonIndex = sb.indexOf(";", semicolonIndex);
}
return sb.toString();
}
private String removeJsessionid(String requestUri) {
@@ -39,6 +39,7 @@ import org.springframework.http.MediaType;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView1;
import org.springframework.http.codec.json.JacksonViewBean.MyJacksonView3;
import org.springframework.http.converter.json.MappingJacksonValue;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.web.testfixture.xml.Pojo;
@@ -214,6 +215,25 @@ public class Jackson2JsonEncoderTests extends AbstractEncoderTests<Jackson2JsonE
null, hints);
}
@Test
public void jacksonValue() {
JacksonViewBean bean = new JacksonViewBean();
bean.setWithView1("with");
bean.setWithView2("with");
bean.setWithoutView("without");
MappingJacksonValue jacksonValue = new MappingJacksonValue(bean);
jacksonValue.setSerializationView(MyJacksonView1.class);
ResolvableType type = ResolvableType.forClass(MappingJacksonValue.class);
testEncode(Mono.just(jacksonValue), type, step -> step
.consumeNextWith(expectString("{\"withView1\":\"with\"}")
.andThen(DataBufferUtils::release))
.verifyComplete(),
null, Collections.emptyMap());
}
@Test // gh-22771
public void encodeWithFlushAfterWriteOff() {
ObjectMapper mapper = new ObjectMapper();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,6 +47,8 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.entry;
import static org.assertj.core.api.Assertions.within;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
/**
* Jackson 2.x converter tests.
@@ -149,6 +151,7 @@ public class MappingJackson2HttpMessageConverterTests {
assertThat(result.contains("\"bool\":true")).isTrue();
assertThat(result.contains("\"bytes\":\"AQI=\"")).isTrue();
assertThat(outputMessage.getHeaders().getContentType()).as("Invalid content-type").isEqualTo(MediaType.APPLICATION_JSON);
verify(outputMessage.getBody(), never()).close();
}
@Test
@@ -55,8 +55,8 @@ class UriComponentsBuilderTests {
void plain() throws URISyntaxException {
UriComponentsBuilder builder = UriComponentsBuilder.newInstance();
UriComponents result = builder.scheme("https").host("example.com")
.path("foo").queryParam("bar").fragment("baz")
.build();
.path("foo").queryParam("bar").fragment("baz").build();
assertThat(result.getScheme()).isEqualTo("https");
assertThat(result.getHost()).isEqualTo("example.com");
assertThat(result.getPath()).isEqualTo("foo");
@@ -93,10 +93,10 @@ class UriComponentsBuilderTests {
@Test
void fromPath() throws URISyntaxException {
UriComponents result = UriComponentsBuilder.fromPath("foo").queryParam("bar").fragment("baz").build();
assertThat(result.getPath()).isEqualTo("foo");
assertThat(result.getQuery()).isEqualTo("bar");
assertThat(result.getFragment()).isEqualTo("baz");
assertThat(result.toUriString()).as("Invalid result URI String").isEqualTo("foo?bar#baz");
URI expected = new URI("foo?bar#baz");
@@ -113,12 +113,12 @@ class UriComponentsBuilderTests {
void fromHierarchicalUri() throws URISyntaxException {
URI uri = new URI("https://example.com/foo?bar#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertThat(result.getScheme()).isEqualTo("https");
assertThat(result.getHost()).isEqualTo("example.com");
assertThat(result.getPath()).isEqualTo("/foo");
assertThat(result.getQuery()).isEqualTo("bar");
assertThat(result.getFragment()).isEqualTo("baz");
assertThat(result.toUri()).as("Invalid result URI").isEqualTo(uri);
}
@@ -126,10 +126,10 @@ class UriComponentsBuilderTests {
void fromOpaqueUri() throws URISyntaxException {
URI uri = new URI("mailto:foo@bar.com#baz");
UriComponents result = UriComponentsBuilder.fromUri(uri).build();
assertThat(result.getScheme()).isEqualTo("mailto");
assertThat(result.getSchemeSpecificPart()).isEqualTo("foo@bar.com");
assertThat(result.getFragment()).isEqualTo("baz");
assertThat(result.toUri()).as("Invalid result URI").isEqualTo(uri);
}
@@ -614,7 +614,7 @@ class UriComponentsBuilderTests {
assertThat(after.getPath()).isEqualTo("/foo/");
}
@Test // gh-19890
@Test // gh-19890
void fromHttpRequestWithEmptyScheme() {
HttpRequest request = new HttpRequest() {
@Override
@@ -896,7 +896,7 @@ class UriComponentsBuilderTests {
assertThat(uriComponents.getQueryParams().get("bar").get(0)).isNull();
}
@Test // gh-24444
@Test // gh-24444
void opaqueUriDoesNotResetOnNullInput() throws URISyntaxException {
URI uri = new URI("urn:ietf:wg:oauth:2.0:oob");
UriComponents result = UriComponentsBuilder.fromUri(uri)
@@ -1156,7 +1156,7 @@ class UriComponentsBuilderTests {
assertThat(result.toUriString()).isEqualTo("https://example.com/rest/mobile/users/1");
}
@Test // gh-25737
@Test // gh-25737
void fromHttpRequestForwardedHeaderComma() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("Forwarded", "for=192.0.2.0,for=192.0.2.1;proto=https;host=192.0.2.3:9090");
@@ -1195,9 +1195,15 @@ class UriComponentsBuilderTests {
assertThat(uri).isEqualTo("http://localhost:8081/{path}?sort={sort}&sort=another_value");
}
@Test // SPR-17630
@Test // SPR-17630
void toUriStringWithCurlyBraces() {
assertThat(UriComponentsBuilder.fromUriString("/path?q={asa}asa").toUriString()).isEqualTo("/path?q=%7Basa%7Dasa");
}
@Test // gh-26012
void verifyDoubleSlashReplacedWithSingleOne() {
String path = UriComponentsBuilder.fromPath("/home/").path("/path").build().getPath();
assertThat(path).isEqualTo("/home/path");
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -43,7 +43,6 @@ public class UriComponentsTests {
@Test
public void expandAndEncode() {
UriComponents uri = UriComponentsBuilder
.fromPath("/hotel list/{city} specials").queryParam("q", "{value}").build()
.expand("Z\u00fcrich", "a+b").encode();
@@ -53,7 +52,6 @@ public class UriComponentsTests {
@Test
public void encodeAndExpand() {
UriComponents uri = UriComponentsBuilder
.fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode().build()
.expand("Z\u00fcrich", "a+b");
@@ -63,16 +61,14 @@ public class UriComponentsTests {
@Test
public void encodeAndExpandPartially() {
UriComponents uri = UriComponentsBuilder
.fromPath("/hotel list/{city} specials").queryParam("q", "{value}").encode()
.uriVariables(Collections.singletonMap("city", "Z\u00fcrich"))
.build();
.uriVariables(Collections.singletonMap("city", "Z\u00fcrich")).build();
assertThat(uri.expand("a+b").toString()).isEqualTo("/hotel%20list/Z%C3%BCrich%20specials?q=a%2Bb");
}
@Test // SPR-17168
@Test // SPR-17168
public void encodeAndExpandWithDollarSign() {
UriComponents uri = UriComponentsBuilder.fromPath("/path").queryParam("q", "{value}").encode().build();
assertThat(uri.expand("JavaClass$1.class").toString()).isEqualTo("/path?q=JavaClass%241.class");
@@ -80,71 +76,71 @@ public class UriComponentsTests {
@Test
public void toUriEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uriComponents.encode().toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uri.encode().toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
}
@Test
public void toUriNotEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uriComponents.toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z\u00fcrich"));
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/hotel list/Z\u00fcrich").build();
assertThat(uri.toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z\u00fcrich"));
}
@Test
public void toUriAlreadyEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com/hotel%20list/Z%C3%BCrich").build(true);
UriComponents encoded = uriComponents.encode();
assertThat(encoded.toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/hotel%20list/Z%C3%BCrich").build(true);
assertThat(uri.encode().toUri()).isEqualTo(new URI("https://example.com/hotel%20list/Z%C3%BCrich"));
}
@Test
public void toUriWithIpv6HostAlreadyEncoded() throws URISyntaxException {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
UriComponents uri = UriComponentsBuilder.fromUriString(
"http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich").build(true);
UriComponents encoded = uriComponents.encode();
assertThat(encoded.toUri()).isEqualTo(new URI("http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich"));
assertThat(uri.encode().toUri()).isEqualTo(
new URI("http://[1abc:2abc:3abc::5ABC:6abc]:8080/hotel%20list/Z%C3%BCrich"));
}
@Test
public void expand() {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
"https://example.com").path("/{foo} {bar}").build();
uriComponents = uriComponents.expand("1 2", "3 4");
assertThat(uriComponents.getPath()).isEqualTo("/1 2 3 4");
assertThat(uriComponents.toUriString()).isEqualTo("https://example.com/1 2 3 4");
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com").path("/{foo} {bar}").build();
uri = uri.expand("1 2", "3 4");
assertThat(uri.getPath()).isEqualTo("/1 2 3 4");
assertThat(uri.toUriString()).isEqualTo("https://example.com/1 2 3 4");
}
@Test // SPR-13311
@Test // SPR-13311
public void expandWithRegexVar() {
String template = "/myurl/{name:[a-z]{1,5}}/show";
UriComponents uriComponents = UriComponentsBuilder.fromUriString(template).build();
uriComponents = uriComponents.expand(Collections.singletonMap("name", "test"));
assertThat(uriComponents.getPath()).isEqualTo("/myurl/test/show");
UriComponents uri = UriComponentsBuilder.fromUriString(template).build();
uri = uri.expand(Collections.singletonMap("name", "test"));
assertThat(uri.getPath()).isEqualTo("/myurl/test/show");
}
@Test // SPR-17630
@Test // SPR-17630
public void uirTemplateExpandWithMismatchedCurlyBraces() {
assertThat(UriComponentsBuilder.fromUriString("/myurl/?q={{{{").encode().build().toUriString()).isEqualTo("/myurl/?q=%7B%7B%7B%7B");
UriComponents uri = UriComponentsBuilder.fromUriString("/myurl/?q={{{{").encode().build();
assertThat(uri.toUriString()).isEqualTo("/myurl/?q=%7B%7B%7B%7B");
}
@Test // gh-22447
@Test // gh-22447
public void expandWithFragmentOrder() {
UriComponents uriComponents = UriComponentsBuilder
UriComponents uri = UriComponentsBuilder
.fromUriString("https://{host}/{path}#{fragment}").build()
.expand("example.com", "foo", "bar");
assertThat(uriComponents.toUriString()).isEqualTo("https://example.com/foo#bar");
assertThat(uri.toUriString()).isEqualTo("https://example.com/foo#bar");
}
@Test // SPR-12123
@Test // SPR-12123
public void port() {
UriComponents uri1 = fromUriString("https://example.com:8080/bar").build();
UriComponents uri2 = fromUriString("https://example.com/bar").port(8080).build();
UriComponents uri3 = fromUriString("https://example.com/bar").port("{port}").build().expand(8080);
UriComponents uri4 = fromUriString("https://example.com/bar").port("808{digit}").build().expand(0);
assertThat(uri1.getPort()).isEqualTo(8080);
assertThat(uri1.toUriString()).isEqualTo("https://example.com:8080/bar");
assertThat(uri2.getPort()).isEqualTo(8080);
@@ -175,20 +171,22 @@ public class UriComponentsTests {
@Test
public void normalize() {
UriComponents uriComponents = UriComponentsBuilder.fromUriString("https://example.com/foo/../bar").build();
assertThat(uriComponents.normalize().toString()).isEqualTo("https://example.com/bar");
UriComponents uri = UriComponentsBuilder.fromUriString("https://example.com/foo/../bar").build();
assertThat(uri.normalize().toString()).isEqualTo("https://example.com/bar");
}
@Test
public void serializable() throws Exception {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(
UriComponents uri = UriComponentsBuilder.fromUriString(
"https://example.com").path("/{foo}").query("bar={baz}").build();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(uriComponents);
oos.writeObject(uri);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()));
UriComponents readObject = (UriComponents) ois.readObject();
assertThat(uriComponents.toString()).isEqualTo(readObject.toString());
assertThat(uri.toString()).isEqualTo(readObject.toString());
}
@Test
@@ -197,6 +195,7 @@ public class UriComponentsTests {
UriComponentsBuilder targetBuilder = UriComponentsBuilder.newInstance();
source.copyToUriComponentsBuilder(targetBuilder);
UriComponents result = targetBuilder.build().encode();
assertThat(result.getPath()).isEqualTo("/foo/bar/ba%2Fz");
assertThat(result.getPathSegments()).isEqualTo(Arrays.asList("foo", "bar", "ba%2Fz"));
}
@@ -207,6 +206,7 @@ public class UriComponentsTests {
UriComponents uric1 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bar={baz}").build();
UriComponents uric2 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bar={baz}").build();
UriComponents uric3 = UriComponentsBuilder.fromUriString(url).path("/{foo}").query("bin={baz}").build();
assertThat(uric1).isInstanceOf(HierarchicalUriComponents.class);
assertThat(uric1).isEqualTo(uric1);
assertThat(uric1).isEqualTo(uric2);
@@ -219,6 +219,7 @@ public class UriComponentsTests {
UriComponents uric1 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bar").build();
UriComponents uric2 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bar").build();
UriComponents uric3 = UriComponentsBuilder.fromUriString(baseUrl + "/foo/bin").build();
assertThat(uric1).isInstanceOf(OpaqueUriComponents.class);
assertThat(uric1).isEqualTo(uric1);
assertThat(uric1).isEqualTo(uric2);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,9 +49,7 @@ public class UriTemplateTests {
assertThat(result).as("Invalid expanded template").isEqualTo(new URI("/hotels/1/bookings/42"));
}
// SPR-9712
@Test
@Test // SPR-9712
public void expandVarArgsWithArrayValue() throws Exception {
UriTemplate template = new UriTemplate("/sum?numbers={numbers}");
URI result = template.expand(new int[] {1, 2, 3});
@@ -61,8 +59,7 @@ public class UriTemplateTests {
@Test
public void expandVarArgsNotEnoughVariables() throws Exception {
UriTemplate template = new UriTemplate("/hotels/{hotel}/bookings/{booking}");
assertThatIllegalArgumentException().isThrownBy(() ->
template.expand("1"));
assertThatIllegalArgumentException().isThrownBy(() -> template.expand("1"));
}
@Test
@@ -156,7 +153,7 @@ public class UriTemplateTests {
assertThat(result).as("Invalid match").isEqualTo(expected);
}
@Test // SPR-13627
@Test // SPR-13627
public void matchCustomRegexWithNestedCurlyBraces() throws Exception {
UriTemplate template = new UriTemplate("/site.{domain:co.[a-z]{2}}");
Map<String, String> result = template.match("/site.co.eu");
@@ -181,8 +178,8 @@ public class UriTemplateTests {
assertThat(result).as("Invalid match").isEqualTo(expected);
}
@Test // SPR-16169
public void matchWithMultipleSegmentsAtTheEnd() {
@Test // SPR-16169
public void matchWithMultipleSegmentsAtTheEnd() throws Exception {
UriTemplate template = new UriTemplate("/account/{accountId}");
assertThat(template.matches("/account/15/alias/5")).isFalse();
}
@@ -202,21 +199,20 @@ public class UriTemplateTests {
assertThat(template.matches("/search?query=foo#bar")).isTrue();
}
@Test // SPR-13705
public void matchesWithSlashAtTheEnd() {
UriTemplate uriTemplate = new UriTemplate("/test/");
assertThat(uriTemplate.matches("/test/")).isTrue();
@Test // SPR-13705
public void matchesWithSlashAtTheEnd() throws Exception {
assertThat(new UriTemplate("/test/").matches("/test/")).isTrue();
}
@Test
public void expandWithDollar() {
public void expandWithDollar() throws Exception {
UriTemplate template = new UriTemplate("/{a}");
URI uri = template.expand("$replacement");
assertThat(uri.toString()).isEqualTo("/$replacement");
}
@Test
public void expandWithAtSign() {
public void expandWithAtSign() throws Exception {
UriTemplate template = new UriTemplate("http://localhost/query={query}");
URI uri = template.expand("foo@bar");
assertThat(uri.toString()).isEqualTo("http://localhost/query=foo@bar");
@@ -104,6 +104,9 @@ public class UrlPathHelperTests {
request.setRequestURI("/foo+bar");
assertThat(helper.getRequestUri(request)).isEqualTo("/foo+bar");
request.setRequestURI("/home/" + "/path");
assertThat(helper.getRequestUri(request)).isEqualTo("/home/path");
}
@Test
@@ -112,6 +115,9 @@ public class UrlPathHelperTests {
request.setRequestURI("/foo;f=F;o=O;o=O/bar;b=B;a=A;r=R");
assertThat(helper.getRequestUri(request)).isEqualTo("/foo/bar");
request.setRequestURI("/foo;f=F;o=O;o=O/bar;b=B;a=A;r=R/baz;test");
assertThat(helper.getRequestUri(request)).isEqualTo("/foo/bar/baz");
// SPR-13455
request.setRequestURI("/foo/;test/1");
request.setServletPath("/foo/1");
@@ -33,6 +33,7 @@ import java.util.function.Supplier;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpHeaders;
@@ -173,6 +174,9 @@ class DefaultWebClient implements WebClient {
private final Map<String, Object> attributes = new LinkedHashMap<>(4);
@Nullable
private Function<Context, Context> contextModifier;
@Nullable
private Consumer<ClientHttpRequest> httpRequestConsumer;
@@ -181,6 +185,7 @@ class DefaultWebClient implements WebClient {
this.httpMethod = httpMethod;
}
@Override
public RequestBodySpec uri(String uriTemplate, Object... uriVariables) {
attribute(URI_TEMPLATE_ATTRIBUTE, uriTemplate);
@@ -238,18 +243,6 @@ class DefaultWebClient implements WebClient {
return this;
}
@Override
public RequestBodySpec attribute(String name, Object value) {
this.attributes.put(name, value);
return this;
}
@Override
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
attributesConsumer.accept(this.attributes);
return this;
}
@Override
public DefaultRequestBodyUriSpec accept(MediaType... acceptableMediaTypes) {
getHeaders().setAccept(Arrays.asList(acceptableMediaTypes));
@@ -298,6 +291,25 @@ class DefaultWebClient implements WebClient {
return this;
}
@Override
public RequestBodySpec attribute(String name, Object value) {
this.attributes.put(name, value);
return this;
}
@Override
public RequestBodySpec attributes(Consumer<Map<String, Object>> attributesConsumer) {
attributesConsumer.accept(this.attributes);
return this;
}
@Override
public RequestBodySpec context(Function<Context, Context> contextModifier) {
this.contextModifier = (this.contextModifier != null ?
this.contextModifier.andThen(contextModifier) : contextModifier);
return this;
}
@Override
public RequestBodySpec httpRequest(Consumer<ClientHttpRequest> requestConsumer) {
this.httpRequestConsumer = (this.httpRequestConsumer != null ?
@@ -412,9 +424,15 @@ class DefaultWebClient implements WebClient {
ClientRequest request = (this.inserter != null ?
initRequestBuilder().body(this.inserter).build() :
initRequestBuilder().build());
return Mono.defer(() -> exchangeFunction.exchange(request)
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR));
return Mono.defer(() -> {
Mono<ClientResponse> responseMono = exchangeFunction.exchange(request)
.checkpoint("Request to " + this.httpMethod.name() + " " + this.uri + " [DefaultWebClient]")
.switchIfEmpty(NO_HTTP_CLIENT_RESPONSE_ERROR);
if (this.contextModifier != null) {
responseMono = responseMono.contextWrite(this.contextModifier);
}
return responseMono;
});
}
private ClientRequest.Builder initRequestBuilder() {
@@ -480,12 +498,14 @@ class DefaultWebClient implements WebClient {
private final List<StatusHandler> statusHandlers = new ArrayList<>(1);
DefaultResponseSpec(Mono<ClientResponse> responseMono, Supplier<HttpRequest> requestSupplier) {
this.responseMono = responseMono;
this.requestSupplier = requestSupplier;
this.statusHandlers.add(DEFAULT_STATUS_HANDLER);
}
@Override
public ResponseSpec onStatus(Predicate<HttpStatus> statusPredicate,
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction) {
@@ -622,6 +642,22 @@ class DefaultWebClient implements WebClient {
handleBodyFlux(response, response.bodyToFlux(elementTypeRef))));
}
@Override
public <T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(Class<T> elementType) {
return this.responseMono.map(response ->
ResponseEntity.status(response.rawStatusCode())
.headers(response.headers().asHttpHeaders())
.body(response.bodyToFlux(elementType)));
}
@Override
public <T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(ParameterizedTypeReference<T> elementTypeReference) {
return this.responseMono.map(response ->
ResponseEntity.status(response.rawStatusCode())
.headers(response.headers().asHttpHeaders())
.body(response.bodyToFlux(elementTypeReference)));
}
@Override
public Mono<ResponseEntity<Void>> toBodilessEntity() {
return this.responseMono.flatMap(response ->
@@ -274,9 +274,13 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
.map(filter -> filter.apply(exchange))
.orElse(exchange) : exchange);
HttpHeaders defaultHeaders = copyDefaultHeaders();
MultiValueMap<String, String> defaultCookies = copyDefaultCookies();
return new DefaultWebClient(filteredExchange, initUriBuilderFactory(),
this.defaultHeaders != null ? HttpHeaders.readOnlyHttpHeaders(this.defaultHeaders) : null,
this.defaultCookies != null ? CollectionUtils.unmodifiableMultiValueMap(this.defaultCookies) : null,
defaultHeaders,
defaultCookies,
this.defaultRequest, new DefaultWebClientBuilder(this));
}
@@ -313,4 +317,28 @@ final class DefaultWebClientBuilder implements WebClient.Builder {
return factory;
}
@Nullable
private HttpHeaders copyDefaultHeaders() {
if (this.defaultHeaders != null) {
HttpHeaders copy = new HttpHeaders();
this.defaultHeaders.forEach((key, values) -> copy.put(key, new ArrayList<>(values)));
return HttpHeaders.readOnlyHttpHeaders(copy);
}
else {
return null;
}
}
@Nullable
private MultiValueMap<String, String> copyDefaultCookies() {
if (this.defaultCookies != null) {
MultiValueMap<String, String> copy = new LinkedMultiValueMap<>(this.defaultCookies.size());
this.defaultCookies.forEach((key, values) -> copy.put(key, new ArrayList<>(values)));
return CollectionUtils.unmodifiableMultiValueMap(copy);
}
else {
return null;
}
}
}
@@ -29,6 +29,7 @@ import java.util.function.Predicate;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.util.context.Context;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.ReactiveAdapterRegistry;
@@ -470,6 +471,17 @@ public interface WebClient {
*/
S attributes(Consumer<Map<String, Object>> attributesConsumer);
/**
* Provide a function to populate the Reactor {@code Context}. In contrast
* to {@link #attribute(String, Object) attributes} which apply only to
* the current request, the Reactor {@code Context} transparently propagates
* to the downstream processing chain which may include other nested or
* successive calls over HTTP or via other reactive clients.
* @param contextModifier the function to modify the context with
* @since 5.3.1
*/
S context(Function<Context, Context> contextModifier);
/**
* Callback for access to the {@link ClientHttpRequest} that in turn
* provides access to the native request of the underlying HTTP library.
@@ -781,103 +793,116 @@ public interface WebClient {
Function<ClientResponse, Mono<? extends Throwable>> exceptionFunction);
/**
* Extract the body to a {@code Mono}. By default, if the response has status code 4xx or
* 5xx, the {@code Mono} will contain a {@link WebClientException}. This can be overridden
* with {@link #onStatus(Predicate, Function)}.
* @param elementClass the expected response body element class
* @param <T> response body type
* @return a mono containing the body, or a {@link WebClientResponseException} if the
* status code is 4xx or 5xx
* Decode the body to the given target type. For an error response (status
* code of 4xx or 5xx), the {@code Mono} emits a {@link WebClientException}.
* Use {@link #onStatus(Predicate, Function)} to customize error response
* handling.
* @param elementClass the type to decode to
* @param <T> the target body type
* @return the decoded body
*/
<T> Mono<T> bodyToMono(Class<T> elementClass);
/**
* Extract the body to a {@code Mono}. By default, if the response has status code 4xx or
* 5xx, the {@code Mono} will contain a {@link WebClientException}. This can be overridden
* with {@link #onStatus(Predicate, Function)}.
* @param elementTypeRef a type reference describing the expected response body element type
* @param <T> response body type
* @return a mono containing the body, or a {@link WebClientResponseException} if the
* status code is 4xx or 5xx
* Variant of {@link #bodyToMono(Class)} with a {@link ParameterizedTypeReference}.
* @param elementTypeRef the type to decode to
* @param <T> the target body type
* @return the decoded body
*/
<T> Mono<T> bodyToMono(ParameterizedTypeReference<T> elementTypeRef);
/**
* Extract the body to a {@code Flux}. By default, if the response has status code 4xx or
* 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden
* with {@link #onStatus(Predicate, Function)}.
* @param elementClass the class of elements in the response
* @param <T> the type of elements in the response
* @return a flux containing the body, or a {@link WebClientResponseException} if the
* status code is 4xx or 5xx
* Decode the body to a {@link Flux} with elements of the given type.
* For an error response (status code of 4xx or 5xx), the {@code Mono}
* emits a {@link WebClientException}. Use {@link #onStatus(Predicate, Function)}
* to customize error response handling.
* @param elementClass the type of element to decode to
* @param <T> the body element type
* @return the decoded body
*/
<T> Flux<T> bodyToFlux(Class<T> elementClass);
/**
* Extract the body to a {@code Flux}. By default, if the response has status code 4xx or
* 5xx, the {@code Flux} will contain a {@link WebClientException}. This can be overridden
* with {@link #onStatus(Predicate, Function)}.
* @param elementTypeRef a type reference describing the expected response body element type
* @param <T> the type of elements in the response
* @return a flux containing the body, or a {@link WebClientResponseException} if the
* status code is 4xx or 5xx
* Variant of {@link #bodyToMono(Class)} with a {@link ParameterizedTypeReference}.
* @param elementTypeRef the type of element to decode to
* @param <T> the body element type
* @return the decoded body
*/
<T> Flux<T> bodyToFlux(ParameterizedTypeReference<T> elementTypeRef);
/**
* Return the response as a delayed {@code ResponseEntity}. By default, if the response has
* status code 4xx or 5xx, the {@code Mono} will contain a {@link WebClientException}. This
* can be overridden with {@link #onStatus(Predicate, Function)}.
* Return a {@code ResponseEntity} with the body decoded to an Object of
* the given type. For an error response (status code of 4xx or 5xx), the
* {@code Mono} emits a {@link WebClientException}. Use
* {@link #onStatus(Predicate, Function)} to customize error response handling.
* @param bodyClass the expected response body type
* @param <T> response body type
* @return {@code Mono} with the {@code ResponseEntity}
* @return the {@code ResponseEntity} with the decoded body
* @since 5.2
*/
<T> Mono<ResponseEntity<T>> toEntity(Class<T> bodyClass);
/**
* Return the response as a delayed {@code ResponseEntity}. By default, if the response has
* status code 4xx or 5xx, the {@code Mono} will contain a {@link WebClientException}. This
* can be overridden with {@link #onStatus(Predicate, Function)}.
* @param bodyTypeReference a type reference describing the expected response body type
* @param <T> response body type
* @return {@code Mono} with the {@code ResponseEntity}
* Variant of {@link #bodyToMono(Class)} with a {@link ParameterizedTypeReference}.
* @param bodyTypeReference the expected response body type
* @param <T> the response body type
* @return the {@code ResponseEntity} with the decoded body
* @since 5.2
*/
<T> Mono<ResponseEntity<T>> toEntity(ParameterizedTypeReference<T> bodyTypeReference);
/**
* Return the response as a delayed list of {@code ResponseEntity}s. By default, if the
* response has status code 4xx or 5xx, the {@code Mono} will contain a
* {@link WebClientException}. This can be overridden with
* {@link #onStatus(Predicate, Function)}.
* @param elementClass the expected response body list element class
* @param <T> the type of elements in the list
* @return {@code Mono} with the list of {@code ResponseEntity}s
* Return a {@code ResponseEntity} with the body decoded to a {@code List}
* of elements of the given type. For an error response (status code of
* 4xx or 5xx), the {@code Mono} emits a {@link WebClientException}.
* Use {@link #onStatus(Predicate, Function)} to customize error response
* handling.
* @param elementClass the type of element to decode the target Flux to
* @param <T> the body element type
* @return the {@code ResponseEntity}
* @since 5.2
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(Class<T> elementClass);
/**
* Return the response as a delayed list of {@code ResponseEntity}s. By default, if the
* response has status code 4xx or 5xx, the {@code Mono} will contain a
* {@link WebClientException}. This can be overridden with
* {@link #onStatus(Predicate, Function)}.
* @param elementTypeRef the expected response body list element reference type
* @param <T> the type of elements in the list
* @return {@code Mono} with the list of {@code ResponseEntity}s
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
* @param elementTypeRef the type of element to decode the target Flux to
* @param <T> the body element type
* @return the {@code ResponseEntity}
* @since 5.2
*/
<T> Mono<ResponseEntity<List<T>>> toEntityList(ParameterizedTypeReference<T> elementTypeRef);
/**
* Return the response as a delayed {@code ResponseEntity} containing status and headers,
* but no body. By default, if the response has status code 4xx or 5xx, the {@code Mono}
* will contain a {@link WebClientException}. This can be overridden with
* {@link #onStatus(Predicate, Function)}.
* Calling this method will {@linkplain ClientResponse#releaseBody() release} the body of
* the response.
* @return {@code Mono} with the bodiless {@code ResponseEntity}
* Return a {@code ResponseEntity} with the body decoded to a {@code Flux}
* of elements of the given type. For an error response (status code of
* 4xx or 5xx), the {@code Mono} emits a {@link WebClientException}.
* Use {@link #onStatus(Predicate, Function)} to customize error response
* handling.
* <p><strong>Note:</strong> The {@code Flux} representing the body must
* be subscribed to or else associated resources will not be released.
* @param elementType the type of element to decode the target Flux to
* @param <T> the body element type
* @return the {@code ResponseEntity}
* @since 5.3.1
*/
<T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(Class<T> elementType);
/**
* Variant of {@link #toEntity(Class)} with a {@link ParameterizedTypeReference}.
* @param elementTypeReference the type of element to decode the target Flux to
* @param <T> the body element type
* @return the {@code ResponseEntity}
* @since 5.3.1
*/
<T> Mono<ResponseEntity<Flux<T>>> toEntityFlux(ParameterizedTypeReference<T> elementTypeReference);
/**
* Return a {@code ResponseEntity} without a body. For an error response
* (status code of 4xx or 5xx), the {@code Mono} emits a
* {@link WebClientException}. Use {@link #onStatus(Predicate, Function)}
* to customize error response handling.
* @return the {@code ResponseEntity}
* @since 5.2
*/
Mono<ResponseEntity<Void>> toBodilessEntity();
@@ -66,7 +66,6 @@ public interface WebSocketSession {
* is closed. In a typical {@link WebSocketHandler} implementation this
* stream is composed into the overall processing flow, so that when the
* connection is closed, handling will end.
*
* <p>See the class-level doc of {@link WebSocketHandler} and the reference
* for more details and examples of how to handle the session.
*/
@@ -76,12 +75,17 @@ public interface WebSocketSession {
* Give a source of outgoing messages, write the messages and return a
* {@code Mono<Void>} that completes when the source completes and writing
* is done.
*
* <p>See the class-level doc of {@link WebSocketHandler} and the reference
* for more details and examples of how to handle the session.
*/
Mono<Void> send(Publisher<WebSocketMessage> messages);
/**
* Whether the underlying connection is open.
* @since 5.3.1
*/
boolean isOpen();
/**
* Close the WebSocket session with {@link CloseStatus#NORMAL}.
*/
@@ -113,6 +113,11 @@ public class JettyWebSocketSession extends AbstractListenerWebSocketSession<Sess
return true;
}
@Override
public boolean isOpen() {
return getDelegate().isOpen();
}
@Override
public Mono<Void> close(CloseStatus status) {
getDelegate().close(status.getCode(), status.getReason());
@@ -15,10 +15,13 @@
*/
package org.springframework.web.reactive.socket.adapter;
import java.util.function.Consumer;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.netty.Connection;
import reactor.netty.NettyInbound;
import reactor.netty.NettyOutbound;
import reactor.netty.http.websocket.WebsocketInbound;
@@ -93,6 +96,13 @@ public class ReactorNettyWebSocketSession
.then();
}
@Override
public boolean isOpen() {
DisposedCallback callback = new DisposedCallback();
getDelegate().getInbound().withConnection(callback);
return callback.isDisposed();
}
@Override
public Mono<Void> close(CloseStatus status) {
// this will notify WebSocketInbound.receiveCloseStatus()
@@ -129,4 +139,19 @@ public class ReactorNettyWebSocketSession
}
}
private static class DisposedCallback implements Consumer<Connection> {
private boolean disposed;
public boolean isDisposed() {
return this.disposed;
}
@Override
public void accept(Connection connection) {
this.disposed = connection.isDisposed();
}
}
}
@@ -103,6 +103,11 @@ public class StandardWebSocketSession extends AbstractListenerWebSocketSession<S
return true;
}
@Override
public boolean isOpen() {
return getDelegate().isOpen();
}
@Override
public Mono<Void> close(CloseStatus status) {
try {
@@ -108,6 +108,11 @@ public class UndertowWebSocketSession extends AbstractListenerWebSocketSession<W
return true;
}
@Override
public boolean isOpen() {
return getDelegate().isOpen();
}
@Override
public Mono<Void> close(CloseStatus status) {
CloseMessage cm = new CloseMessage(status.getCode(), status.getReason());
@@ -129,6 +129,34 @@ public class DefaultWebClientTests {
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
}
@Test
public void contextFromThreadLocal() {
WebClient client = this.builder
.filter((request, next) ->
// Async, continue on different thread
Mono.delay(Duration.ofMillis(10)).then(next.exchange(request)))
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String fooValue = contextView.get("foo");
return next.exchange(ClientRequest.from(request).header("foo", fooValue).build());
}))
.build();
ThreadLocal<String> fooHolder = new ThreadLocal<>();
fooHolder.set("bar");
try {
client.get().uri("/path")
.context(context -> context.put("foo", fooHolder.get()))
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10));
}
finally {
fooHolder.remove();
}
ClientRequest request = verifyAndGetRequest();
assertThat(request.headers().getFirst("foo")).isEqualTo("bar");
}
@Test
public void httpRequest() {
this.builder.build().get().uri("/path")
@@ -142,7 +170,8 @@ public class DefaultWebClientTests {
@Test
public void defaultHeaderAndCookie() {
WebClient client = this.builder
.defaultHeader("Accept", "application/json").defaultCookie("id", "123")
.defaultHeader("Accept", "application/json")
.defaultCookie("id", "123")
.build();
client.get().uri("/path")
@@ -170,6 +199,33 @@ public class DefaultWebClientTests {
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
}
@Test
public void defaultHeaderAndCookieCopies() {
WebClient client1 = this.builder
.defaultHeader("Accept", "application/json")
.defaultCookie("id", "123")
.build();
WebClient client2 = this.builder
.defaultHeader("Accept", "application/xml")
.defaultCookies(cookies -> cookies.set("id", "456"))
.build();
client1.get().uri("/path")
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10));
ClientRequest request = verifyAndGetRequest();
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/json");
assertThat(request.cookies().getFirst("id")).isEqualTo("123");
client2.get().uri("/path")
.retrieve().bodyToMono(Void.class).block(Duration.ofSeconds(10));
request = verifyAndGetRequest();
assertThat(request.headers().getFirst("Accept")).isEqualTo("application/xml");
assertThat(request.cookies().getFirst("id")).isEqualTo("456");
}
@Test
public void defaultRequest() {
ThreadLocal<String> context = new NamedThreadLocal<>("foo");
@@ -279,7 +279,7 @@ class WebClientIntegrationTests {
}
@ParameterizedWebClientTest
void retrieveJsonArrayAsResponseEntity(ClientHttpConnector connector) {
void retrieveJsonArrayAsResponseEntityList(ClientHttpConnector connector) {
startServer(connector);
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
@@ -309,6 +309,39 @@ class WebClientIntegrationTests {
});
}
@ParameterizedWebClientTest
void retrieveJsonArrayAsResponseEntityFlux(ClientHttpConnector connector) {
startServer(connector);
String content = "[{\"bar\":\"bar1\",\"foo\":\"foo1\"}, {\"bar\":\"bar2\",\"foo\":\"foo2\"}]";
prepareResponse(response -> response
.setHeader("Content-Type", "application/json").setBody(content));
ResponseEntity<Flux<Pojo>> entity = this.webClient.get()
.uri("/json").accept(MediaType.APPLICATION_JSON)
.retrieve()
.toEntityFlux(Pojo.class)
.block(Duration.ofSeconds(3));
assertThat(entity).isNotNull();
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
assertThat(entity.getHeaders().getContentLength()).isEqualTo(58);
assertThat(entity.getBody()).isNotNull();
StepVerifier.create(entity.getBody())
.expectNext(new Pojo("foo1", "bar1"))
.expectNext(new Pojo("foo2", "bar2"))
.expectComplete()
.verify(Duration.ofSeconds(3));
expectRequestCount(1);
expectRequest(request -> {
assertThat(request.getPath()).isEqualTo("/json");
assertThat(request.getHeader(HttpHeaders.ACCEPT)).isEqualTo("application/json");
});
}
@Test // gh-24788
void retrieveJsonArrayAsBodilessEntityShouldReleasesConnection() {
@@ -1,157 +0,0 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.reactive.function.server;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import reactor.core.publisher.Mono;
import org.springframework.core.io.Resource;
import org.springframework.lang.Nullable;
/**
* @author Arjen Poutsma
*/
public class CustomRouteBuilder {
public static final String OPERATION_ATTRIBUTE = CustomRouteBuilder.class.getName() + ".operation";
private final RouterFunctions.Builder delegate = RouterFunctions.route();
private CustomRouteBuilder() {
}
public static CustomRouteBuilder route() {
return new CustomRouteBuilder();
}
public CustomRouteBuilder GET(String pattern, HandlerFunction<ServerResponse> handlerFunction,
Consumer<OperationBuilder> operationsConsumer) {
OperationBuilder builder = new OperationBuilder();
operationsConsumer.accept(builder);
this.delegate.GET(pattern, handlerFunction)
.withAttribute(OPERATION_ATTRIBUTE, builder.operation);
return this;
}
public RouterFunction<ServerResponse> build() {
return this.delegate.build();
}
public static void main(String[] args) {
RouterFunction<ServerResponse> routerFunction =
route()
.GET("/foo", request -> ServerResponse.ok().build(), ops -> ops
.parameter("key1", "My key1 description")
.parameter("key1", "My key1 description")
.response(200, "This is normal response description")
.response(404, "This is response description")
)
.build();
AttributesVisitor visitor = new AttributesVisitor();
routerFunction.accept(visitor);
}
public static class OperationBuilder {
private final Operation operation = new Operation();
public OperationBuilder parameter(String name, String description) {
this.operation.parameter(name, description);
return this;
}
public OperationBuilder response(int statusCode, String description) {
this.operation.response(statusCode, description);
return this;
}
}
static class Operation {
private final Map<String, String> parameters = new LinkedHashMap<>();
private final Map<Integer, String > responses = new LinkedHashMap<>();
public void parameter(String name, String description) {
this.parameters.put(name, description);
}
public void response(int status, String description) {
this.responses.put(status, description);
}
@Override
public String toString() {
return "parameters=" + parameters +
", responses=" + responses;
}
}
static class AttributesVisitor implements RouterFunctions.Visitor {
@Nullable
private Map<String, Object> attributes;
@Override
public void attributes(Map<String, Object> attributes) {
this.attributes = attributes;
}
@Override
public void route(RequestPredicate predicate, HandlerFunction<?> handlerFunction) {
System.out.printf("Route predicate %s->%s%nhas attributes %s", predicate, handlerFunction, this.attributes);
this.attributes = null;
}
@Override
public void startNested(RequestPredicate predicate) {
// TODO
}
@Override
public void endNested(RequestPredicate predicate) {
// TODO
}
@Override
public void resources(Function<ServerRequest, Mono<Resource>> lookupFunction) {
// TODO
}
@Override
public void unknown(RouterFunction<?> routerFunction) {
// TODO
}
}
}
@@ -162,7 +162,7 @@ class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
private static final boolean javaxValidationPresent;
private static boolean romePresent;
private static final boolean romePresent;
private static final boolean jaxb2Present;
@@ -19,9 +19,11 @@ package org.springframework.web.servlet.config;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.RuntimeBeanReference;
import org.springframework.beans.factory.parsing.BeanComponentDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.lang.Nullable;
@@ -43,6 +45,7 @@ import org.springframework.web.util.UrlPathHelper;
* Convenience methods for use in MVC namespace BeanDefinitionParsers.
*
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @author Brian Clozel
* @author Marten Deinum
* @since 3.1
@@ -67,15 +70,15 @@ public abstract class MvcNamespaceUtils {
private static final String HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME = "mvcHandlerMappingIntrospector";
public static void registerDefaultComponents(ParserContext parserContext, @Nullable Object source) {
registerBeanNameUrlHandlerMapping(parserContext, source);
registerHttpRequestHandlerAdapter(parserContext, source);
registerSimpleControllerHandlerAdapter(parserContext, source);
registerHandlerMappingIntrospector(parserContext, source);
registerThemeResolver(parserContext, source);
registerLocaleResolver(parserContext, source);
registerFlashMapManager(parserContext, source);
registerViewNameTranslator(parserContext, source);
public static void registerDefaultComponents(ParserContext context, @Nullable Object source) {
registerBeanNameUrlHandlerMapping(context, source);
registerHttpRequestHandlerAdapter(context, source);
registerSimpleControllerHandlerAdapter(context, source);
registerHandlerMappingIntrospector(context, source);
registerLocaleResolver(context, source);
registerThemeResolver(context, source);
registerViewNameTranslator(context, source);
registerFlashMapManager(context, source);
}
/**
@@ -84,21 +87,21 @@ public abstract class MvcNamespaceUtils {
* @return a RuntimeBeanReference to this {@link UrlPathHelper} instance
*/
public static RuntimeBeanReference registerUrlPathHelper(
@Nullable RuntimeBeanReference urlPathHelperRef, ParserContext parserContext, @Nullable Object source) {
@Nullable RuntimeBeanReference urlPathHelperRef, ParserContext context, @Nullable Object source) {
if (urlPathHelperRef != null) {
if (parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) {
parserContext.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME);
if (context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME)) {
context.getRegistry().removeAlias(URL_PATH_HELPER_BEAN_NAME);
}
parserContext.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME);
context.getRegistry().registerAlias(urlPathHelperRef.getBeanName(), URL_PATH_HELPER_BEAN_NAME);
}
else if (!parserContext.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&
!parserContext.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) {
else if (!context.getRegistry().isAlias(URL_PATH_HELPER_BEAN_NAME) &&
!context.getRegistry().containsBeanDefinition(URL_PATH_HELPER_BEAN_NAME)) {
RootBeanDefinition urlPathHelperDef = new RootBeanDefinition(UrlPathHelper.class);
urlPathHelperDef.setSource(source);
urlPathHelperDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
parserContext.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef);
parserContext.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME));
context.getRegistry().registerBeanDefinition(URL_PATH_HELPER_BEAN_NAME, urlPathHelperDef);
context.registerComponent(new BeanComponentDefinition(urlPathHelperDef, URL_PATH_HELPER_BEAN_NAME));
}
return new RuntimeBeanReference(URL_PATH_HELPER_BEAN_NAME);
}
@@ -109,21 +112,21 @@ public abstract class MvcNamespaceUtils {
* @return a RuntimeBeanReference to this {@link PathMatcher} instance
*/
public static RuntimeBeanReference registerPathMatcher(@Nullable RuntimeBeanReference pathMatcherRef,
ParserContext parserContext, @Nullable Object source) {
ParserContext context, @Nullable Object source) {
if (pathMatcherRef != null) {
if (parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
parserContext.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);
if (context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME)) {
context.getRegistry().removeAlias(PATH_MATCHER_BEAN_NAME);
}
parserContext.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);
context.getRegistry().registerAlias(pathMatcherRef.getBeanName(), PATH_MATCHER_BEAN_NAME);
}
else if (!parserContext.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
!parserContext.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
else if (!context.getRegistry().isAlias(PATH_MATCHER_BEAN_NAME) &&
!context.getRegistry().containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) {
RootBeanDefinition pathMatcherDef = new RootBeanDefinition(AntPathMatcher.class);
pathMatcherDef.setSource(source);
pathMatcherDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
parserContext.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef);
parserContext.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME));
context.getRegistry().registerBeanDefinition(PATH_MATCHER_BEAN_NAME, pathMatcherDef);
context.registerComponent(new BeanComponentDefinition(pathMatcherDef, PATH_MATCHER_BEAN_NAME));
}
return new RuntimeBeanReference(PATH_MATCHER_BEAN_NAME);
}
@@ -204,28 +207,14 @@ public abstract class MvcNamespaceUtils {
* Registers an {@link HandlerMappingIntrospector} under a well-known name
* unless already registered.
*/
private static void registerHandlerMappingIntrospector(ParserContext parserContext, @Nullable Object source) {
if (!parserContext.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
private static void registerHandlerMappingIntrospector(ParserContext context, @Nullable Object source) {
if (!context.getRegistry().containsBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(HandlerMappingIntrospector.class);
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
beanDef.setLazyInit(true);
parserContext.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);
parserContext.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));
}
}
/**
* Registers an {@link FixedThemeResolver} under a well-known name
* unless already registered.
*/
private static void registerThemeResolver(ParserContext parserContext, @Nullable Object source) {
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(FixedThemeResolver.class);
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, beanDef);
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.THEME_RESOLVER_BEAN_NAME));
context.getRegistry().registerBeanDefinition(HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME, beanDef);
context.registerComponent(new BeanComponentDefinition(beanDef, HANDLER_MAPPING_INTROSPECTOR_BEAN_NAME));
}
}
@@ -233,27 +222,27 @@ public abstract class MvcNamespaceUtils {
* Registers an {@link AcceptHeaderLocaleResolver} under a well-known name
* unless already registered.
*/
private static void registerLocaleResolver(ParserContext parserContext, @Nullable Object source) {
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)) {
private static void registerLocaleResolver(ParserContext context, @Nullable Object source) {
if (!containsBeanInHierarchy(context, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(AcceptHeaderLocaleResolver.class);
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, beanDef);
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME));
context.getRegistry().registerBeanDefinition(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, beanDef);
context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME));
}
}
/**
* Registers an {@link SessionFlashMapManager} under a well-known name
* Registers an {@link FixedThemeResolver} under a well-known name
* unless already registered.
*/
private static void registerFlashMapManager(ParserContext parserContext, @Nullable Object source) {
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(SessionFlashMapManager.class);
private static void registerThemeResolver(ParserContext context, @Nullable Object source) {
if (!containsBeanInHierarchy(context, DispatcherServlet.THEME_RESOLVER_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(FixedThemeResolver.class);
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, beanDef);
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME));
context.getRegistry().registerBeanDefinition(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, beanDef);
context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.THEME_RESOLVER_BEAN_NAME));
}
}
@@ -261,13 +250,29 @@ public abstract class MvcNamespaceUtils {
* Registers an {@link DefaultRequestToViewNameTranslator} under a well-known name
* unless already registered.
*/
private static void registerViewNameTranslator(ParserContext parserContext, @Nullable Object source) {
if (!parserContext.getRegistry().containsBeanDefinition(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)) {
private static void registerViewNameTranslator(ParserContext context, @Nullable Object source) {
if (!containsBeanInHierarchy(context, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(DefaultRequestToViewNameTranslator.class);
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
parserContext.getRegistry().registerBeanDefinition(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, beanDef);
parserContext.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME));
context.getRegistry().registerBeanDefinition(
DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, beanDef);
context.registerComponent(
new BeanComponentDefinition(beanDef, DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME));
}
}
/**
* Registers an {@link SessionFlashMapManager} under a well-known name
* unless already registered.
*/
private static void registerFlashMapManager(ParserContext context, @Nullable Object source) {
if (!containsBeanInHierarchy(context, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME)) {
RootBeanDefinition beanDef = new RootBeanDefinition(SessionFlashMapManager.class);
beanDef.setSource(source);
beanDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
context.getRegistry().registerBeanDefinition(DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME, beanDef);
context.registerComponent(new BeanComponentDefinition(beanDef, DispatcherServlet.FLASH_MAP_MANAGER_BEAN_NAME));
}
}
@@ -290,4 +295,16 @@ public abstract class MvcNamespaceUtils {
return null;
}
/**
* Check for an existing bean of the given name, ideally in the entire
* context hierarchy (through a {@code containsBean} call) since this
* is also what {@code DispatcherServlet} does, or otherwise just in
* the local context (through {@code containsBeanDefinition}).
*/
private static boolean containsBeanInHierarchy(ParserContext context, String beanName) {
BeanDefinitionRegistry registry = context.getRegistry();
return (registry instanceof BeanFactory ? ((BeanFactory) registry).containsBean(beanName) :
registry.containsBeanDefinition(beanName));
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,7 +81,7 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
private static final String RESOURCE_URL_PROVIDER = "mvcResourceUrlProvider";
private static final boolean isWebJarsAssetLocatorPresent = ClassUtils.isPresent(
private static final boolean webJarsPresent = ClassUtils.isPresent(
"org.webjars.WebJarAssetLocator", ResourcesBeanDefinitionParser.class.getClassLoader());
@@ -331,7 +331,7 @@ class ResourcesBeanDefinitionParser implements BeanDefinitionParser {
}
if (isAutoRegistration) {
if (isWebJarsAssetLocatorPresent) {
if (webJarsPresent) {
RootBeanDefinition webJarsResolverDef = new RootBeanDefinition(WebJarsResourceResolver.class);
webJarsResolverDef.setSource(source);
webJarsResolverDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -284,7 +284,7 @@ public class ViewResolverRegistry {
protected List<ViewResolver> getViewResolvers() {
if (this.contentNegotiatingResolver != null) {
return Collections.<ViewResolver>singletonList(this.contentNegotiatingResolver);
return Collections.singletonList(this.contentNegotiatingResolver);
}
else {
return this.viewResolvers;
@@ -0,0 +1,64 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.security.Principal;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
/**
* Resolves an argument of type {@link Principal}, similar to
* {@link ServletRequestMethodArgumentResolver} but irrespective of whether the
* argument is annotated or not. This is done to enable custom argument
* resolution of a {@link Principal} argument (with a custom annotation).
*
* @author Rossen Stoyanchev
* @since 5.3.1
*/
public class PrincipalMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return Principal.class.isAssignableFrom(parameter.getParameterType());
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request == null) {
throw new IllegalStateException("Current request is not of type HttpServletRequest: " + webRequest);
}
Principal principal = request.getUserPrincipal();
if (principal != null && !parameter.getParameterType().isInstance(principal)) {
throw new IllegalStateException("Current user principal is not of type [" +
parameter.getParameterType().getName() + "]: " + principal);
}
return principal;
}
}
@@ -715,6 +715,7 @@ public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
}
// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
return resolvers;
@@ -50,7 +50,9 @@ import org.springframework.web.servlet.support.RequestContextUtils;
* <li>{@link MultipartRequest}
* <li>{@link HttpSession}
* <li>{@link PushBuilder} (as of Spring 5.0 on Servlet 4.0)
* <li>{@link Principal}
* <li>{@link Principal} but only if not annotated in order to allow custom
* resolvers to resolve it, and the falling back on
* {@link PrincipalMethodArgumentResolver}.
* <li>{@link InputStream}
* <li>{@link Reader}
* <li>{@link HttpMethod} (as of Spring 4.0)
@@ -398,7 +398,7 @@ public class RedirectView extends AbstractUrlBasedView implements SmartView {
private Map<String, String> getCurrentRequestUriVariables(HttpServletRequest request) {
String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
Map<String, String> uriVars = (Map<String, String>) request.getAttribute(name);
return (uriVars != null) ? uriVars : Collections.<String, String> emptyMap();
return (uriVars != null) ? uriVars : Collections.emptyMap();
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -84,7 +84,7 @@ public class DispatcherServletTests {
@BeforeEach
public void setUp() throws ServletException {
public void setup() throws ServletException {
MockServletConfig complexConfig = new MockServletConfig(getServletContext(), "complex");
complexConfig.addInitParameter("publishContext", "false");
complexConfig.addInitParameter("class", "notWritable");
@@ -105,6 +105,7 @@ public class DispatcherServletTests {
return servletConfig.getServletContext();
}
@Test
public void configuredDispatcherServlets() {
assertThat(("simple" + FrameworkServlet.DEFAULT_NAMESPACE_SUFFIX).equals(simpleDispatcherServlet.getNamespace())).as("Correct namespace").isTrue();
@@ -50,6 +50,7 @@ import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.Ordered;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.io.Resource;
@@ -100,6 +101,7 @@ import org.springframework.web.servlet.handler.HandlerMappingIntrospector;
import org.springframework.web.servlet.handler.MappedInterceptor;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor;
import org.springframework.web.servlet.i18n.CookieLocaleResolver;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter;
import org.springframework.web.servlet.mvc.ParameterizableViewController;
@@ -122,9 +124,12 @@ import org.springframework.web.servlet.resource.ResourceUrlProvider;
import org.springframework.web.servlet.resource.ResourceUrlProviderExposingInterceptor;
import org.springframework.web.servlet.resource.VersionResourceResolver;
import org.springframework.web.servlet.resource.WebJarsResourceResolver;
import org.springframework.web.servlet.support.SessionFlashMapManager;
import org.springframework.web.servlet.theme.CookieThemeResolver;
import org.springframework.web.servlet.theme.ThemeChangeInterceptor;
import org.springframework.web.servlet.view.BeanNameViewResolver;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver;
import org.springframework.web.servlet.view.DefaultRequestToViewNameTranslator;
import org.springframework.web.servlet.view.InternalResourceView;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.RedirectView;
@@ -225,10 +230,10 @@ public class MvcNamespaceTests {
assertThat(appContext.getBean(ConversionService.class)).isNotNull();
assertThat(appContext.getBean(LocalValidatorFactoryBean.class)).isNotNull();
assertThat(appContext.getBean(Validator.class)).isNotNull();
assertThat(appContext.getBean("themeResolver", ThemeResolver.class)).isNotNull();
assertThat(appContext.getBean("localeResolver", LocaleResolver.class)).isNotNull();
assertThat(appContext.getBean("flashMapManager", FlashMapManager.class)).isNotNull();
assertThat(appContext.getBean("themeResolver", ThemeResolver.class)).isNotNull();
assertThat(appContext.getBean("viewNameTranslator", RequestToViewNameTranslator.class)).isNotNull();
assertThat(appContext.getBean("flashMapManager", FlashMapManager.class)).isNotNull();
// default web binding initializer behavior test
request = new MockHttpServletRequest("GET", "/");
@@ -262,6 +267,23 @@ public class MvcNamespaceTests {
assertThat(introspector.getHandlerMappings().get(1).getClass()).isEqualTo(BeanNameUrlHandlerMapping.class);
}
@Test // gh-25290
public void testDefaultConfigWithBeansInParentContext() throws Exception {
StaticApplicationContext parent = new StaticApplicationContext();
parent.registerSingleton("localeResolver", CookieLocaleResolver.class);
parent.registerSingleton("themeResolver", CookieThemeResolver.class);
parent.registerSingleton("viewNameTranslator", DefaultRequestToViewNameTranslator.class);
parent.registerSingleton("flashMapManager", SessionFlashMapManager.class);
parent.refresh();
appContext.setParent(parent);
loadBeanDefinitions("mvc-config.xml");
assertThat(appContext.getBean("localeResolver")).isSameAs(parent.getBean("localeResolver"));
assertThat(appContext.getBean("themeResolver")).isSameAs(parent.getBean("themeResolver"));
assertThat(appContext.getBean("viewNameTranslator")).isSameAs(parent.getBean("viewNameTranslator"));
assertThat(appContext.getBean("flashMapManager")).isSameAs(parent.getBean("flashMapManager"));
}
@Test
public void testCustomConversionService() throws Exception {
loadBeanDefinitions("mvc-config-custom-conversion-service.xml");
@@ -0,0 +1,110 @@
/*
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.web.servlet.mvc.method.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.security.Principal;
import javax.servlet.ServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.core.MethodParameter;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.testfixture.servlet.MockHttpServletRequest;
import org.springframework.web.testfixture.servlet.MockHttpServletResponse;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Unit tests for {@link PrincipalMethodArgumentResolver}.
*
* @author Rossen Stoyanchev
*/
public class PrincipalMethodArgumentResolverTests {
private PrincipalMethodArgumentResolver resolver;
private ModelAndViewContainer mavContainer;
private MockHttpServletRequest servletRequest;
private ServletWebRequest webRequest;
private Method method;
@BeforeEach
public void setup() throws Exception {
resolver = new PrincipalMethodArgumentResolver();
mavContainer = new ModelAndViewContainer();
servletRequest = new MockHttpServletRequest("GET", "");
webRequest = new ServletWebRequest(servletRequest, new MockHttpServletResponse());
method = getClass().getMethod("supportedParams", ServletRequest.class, Principal.class);
}
@Test
public void principal() throws Exception {
Principal principal = () -> "Foo";
servletRequest.setUserPrincipal(principal);
MethodParameter principalParameter = new MethodParameter(method, 1);
assertThat(resolver.supportsParameter(principalParameter)).as("Principal not supported").isTrue();
Object result = resolver.resolveArgument(principalParameter, null, webRequest, null);
assertThat(result).as("Invalid result").isSameAs(principal);
}
@Test
public void principalAsNull() throws Exception {
MethodParameter principalParameter = new MethodParameter(method, 1);
assertThat(resolver.supportsParameter(principalParameter)).as("Principal not supported").isTrue();
Object result = resolver.resolveArgument(principalParameter, null, webRequest, null);
assertThat(result).as("Invalid result").isNull();
}
@Test // gh-25780
public void annotatedPrincipal() throws Exception {
Principal principal = () -> "Foo";
servletRequest.setUserPrincipal(principal);
Method principalMethod = getClass().getMethod("supportedParamsWithAnnotatedPrincipal", Principal.class);
MethodParameter principalParameter = new MethodParameter(principalMethod, 0);
assertThat(resolver.supportsParameter(principalParameter)).isTrue();
}
@SuppressWarnings("unused")
public void supportedParams(ServletRequest p0, Principal p1) {}
@Target({ ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticationPrincipal {}
@SuppressWarnings("unused")
public void supportedParamsWithAnnotatedPrincipal(@AuthenticationPrincipal Principal p) {}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -120,7 +120,6 @@ public interface WebSocketSession extends Closeable {
/**
* Send a WebSocket message: either {@link TextMessage} or {@link BinaryMessage}.
*
* <p><strong>Note:</strong> The underlying standard WebSocket session (JSR-356) does
* not allow concurrent sending. Therefore sending must be synchronized. To ensure
* that, one option is to wrap the {@code WebSocketSession} with the
@@ -131,7 +130,7 @@ public interface WebSocketSession extends Closeable {
void sendMessage(WebSocketMessage<?> message) throws IOException;
/**
* Return whether the connection is still open.
* Whether the underlying connection is open.
*/
boolean isOpen();
@@ -27,6 +27,7 @@ import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.user.DestinationUserNameProvider;
import org.springframework.messaging.simp.user.SimpSession;
@@ -34,7 +35,6 @@ import org.springframework.messaging.simp.user.SimpSubscription;
import org.springframework.messaging.simp.user.SimpSubscriptionMatcher;
import org.springframework.messaging.simp.user.SimpUser;
import org.springframework.messaging.simp.user.SimpUserRegistry;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.util.Assert;
/**
@@ -84,19 +84,16 @@ public class DefaultSimpUserRegistry implements SimpUserRegistry, SmartApplicati
public void onApplicationEvent(ApplicationEvent event) {
AbstractSubProtocolEvent subProtocolEvent = (AbstractSubProtocolEvent) event;
Message<?> message = subProtocolEvent.getMessage();
MessageHeaders headers = message.getHeaders();
SimpMessageHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, SimpMessageHeaderAccessor.class);
Assert.state(accessor != null, "No SimpMessageHeaderAccessor");
String sessionId = accessor.getSessionId();
String sessionId = SimpMessageHeaderAccessor.getSessionId(headers);
Assert.state(sessionId != null, "No session id");
if (event instanceof SessionSubscribeEvent) {
LocalSimpSession session = this.sessions.get(sessionId);
if (session != null) {
String id = accessor.getSubscriptionId();
String destination = accessor.getDestination();
String id = SimpMessageHeaderAccessor.getSubscriptionId(headers);
String destination = SimpMessageHeaderAccessor.getDestination(headers);
if (id != null && destination != null) {
session.addSubscription(id, destination);
}
@@ -137,7 +134,7 @@ public class DefaultSimpUserRegistry implements SimpUserRegistry, SmartApplicati
else if (event instanceof SessionUnsubscribeEvent) {
LocalSimpSession session = this.sessions.get(sessionId);
if (session != null) {
String subscriptionId = accessor.getSubscriptionId();
String subscriptionId = SimpMessageHeaderAccessor.getSubscriptionId(headers);
if (subscriptionId != null) {
session.removeSubscription(subscriptionId);
}
@@ -269,13 +269,15 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
}
for (Message<byte[]> message : messages) {
try {
StompHeaderAccessor headerAccessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
Assert.state(headerAccessor != null, "No StompHeaderAccessor");
StompHeaderAccessor headerAccessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
Assert.state(headerAccessor != null, "No StompHeaderAccessor");
StompCommand command = headerAccessor.getCommand();
boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);
StompCommand command = headerAccessor.getCommand();
boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);
boolean sent = false;
try {
headerAccessor.setSessionId(session.getId());
headerAccessor.setSessionAttributes(session.getAttributes());
@@ -305,7 +307,7 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
try {
SimpAttributesContextHolder.setAttributesFromMessage(message);
boolean sent = outputChannel.send(message);
sent = outputChannel.send(message);
if (sent) {
if (this.eventPublisher != null) {
@@ -327,9 +329,15 @@ public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationE
}
}
catch (Throwable ex) {
if (logger.isErrorEnabled()) {
logger.error("Failed to send client message to application via MessageChannel" +
" in session " + session.getId() + ". Sending STOMP ERROR to client.", ex);
if (logger.isDebugEnabled()) {
logger.debug("Failed to send message to MessageChannel in session " + session.getId(), ex);
}
else if (logger.isErrorEnabled()) {
// Skip unsent CONNECT messages (likely auth issues)
if (!isConnect || sent) {
logger.error("Failed to send message to MessageChannel in session " + session.getId() +
":" + ex.getMessage());
}
}
handleError(session, ex, message);
}
+1 -1
View File
@@ -563,7 +563,7 @@ files named `services.xml` and `daos.xml` (which are on the classpath) can be in
val ctx = ClassPathXmlApplicationContext(arrayOf("services.xml", "daos.xml"), MessengerService::class.java)
----
See the api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]
See the {api-spring-framework}/context/support/ClassPathXmlApplicationContext.html[`ClassPathXmlApplicationContext`]
javadoc for details on the various constructors.
+1 -1
View File
@@ -6805,7 +6805,7 @@ The following example extracts the `id` column and emits its value:
.Kotlin
----
val names = client.sql("SELECT name FROM person")
.map{ it.get("id", String.class) }
.map{ row: Row -> row.get("id", String.class) }
.flow()
----
+68 -25
View File
@@ -102,7 +102,7 @@ To change the limit for default codecs, use the following:
.Java
----
WebClient webClient = WebClient.builder()
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024));
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
@@ -831,7 +831,7 @@ inline-style, through the built-in `BodyInserters`, as the following example sho
[[webflux-client-filter]]
== Client Filters
== Filters
You can register a client filter (`ExchangeFilterFunction`) through the `WebClient.Builder`
in order to intercept and modify requests, as the following example shows:
@@ -887,9 +887,36 @@ a filter for basic authentication through a static factory method:
.build()
----
Filters apply globally to every request. To change a filter's behavior for a specific
request, you can add request attributes to the `ClientRequest` that can then be accessed
by all filters in the chain, as the following example shows:
You can create a new `WebClient` instance by using another as a starting point. This allows
insert or removing filters without affecting the original `WebClient`. Below is an example
that inserts a basic authentication filter at index 0:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
----
[[webflux-client-attributes]]
== Attributes
You can add attributes to a request. This is convenient if you want to pass information
through the filter chain and influence the behavior of filters for a given request.
For example:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
@@ -912,10 +939,11 @@ by all filters in the chain, as the following example shows:
.Kotlin
----
val client = WebClient.builder()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
}.build()
.filter { request, _ ->
val usr = request.attributes()["myAttribute"];
// ...
}
.build()
client.get().uri("https://example.org/")
.attribute("myAttribute", "...")
@@ -923,29 +951,44 @@ by all filters in the chain, as the following example shows:
.awaitBody<Unit>()
----
You can also replicate an existing `WebClient`, insert new filters, or remove already
registered filters. The following example, inserts a basic authentication filter at
index 0:
[[webflux-client-context]]
== Context
<<webflux-client-attributes>> provide a convenient way to pass information to the filter
chain but they only influence the current request. If you want to pass information that
propagates to additional requests that are nested, e.g. via `flatMap`, or executed after,
e.g. via `concatMap`, then you'll need to use the Reactor `Context`.
`WebClient` exposes a method to populate the Reactor `Context` for a given request.
This information is available to filters for the current request and it also propagates
to subsequent requests or other reactive clients participating in the downstream
processing chain. For example:
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
.Java
----
import static org.springframework.web.reactive.function.client.ExchangeFilterFunctions.basicAuthentication;
WebClient client = webClient.mutate()
.filters(filterList -> {
filterList.add(0, basicAuthentication("user", "password"));
})
WebClient client = WebClient.builder()
.filter((request, next) ->
Mono.deferContextual(contextView -> {
String value = contextView.get("foo");
// ...
}))
.build();
----
[source,kotlin,indent=0,subs="verbatim,quotes",role="secondary"]
.Kotlin
----
val client = webClient.mutate()
.filters { it.add(0, basicAuthentication("user", "password")) }
.build()
client.get().uri("https://example.org/")
.context(context -> context.put("foo", ...))
.retrieve()
.bodyToMono(String.class)
.flatMap(body -> {
// perform nested request (context propagates automatically)...
});
----
Note that you can also specify how to populate the context through the `defaultRequest`
method at the level of the `WebClient.Builder` and that applies to all requests.
This could be used for to example to pass information from `ThreadLocal` storage onto
a Reactor processing chain in a Spring MVC application.
[[webflux-client-synchronous]]