Compare commits

...

29 Commits

Author SHA1 Message Date
Spring Builds e69dde8291 Release v6.0.22 2024-06-13 09:09:12 +00:00
Juergen Hoeller f285df692c Upgrade to SLF4J 2.0.13, JRuby 9.4.7, Awaitility 4.2.1 2024-06-12 14:42:49 +02:00
Juergen Hoeller b9eeee8341 Polishing 2024-06-12 13:59:33 +02:00
Juergen Hoeller df1f4812aa Upgrade to Netty 4.1.111 2024-06-12 13:58:57 +02:00
Juergen Hoeller 230848c8e5 Upgrade to spring-javaformat-checkstyle 0.0.42 2024-06-12 13:58:27 +02:00
Stéphane Nicoll eda868792a Upgrade to Reactor 2022.0.20
Closes gh-33008
2024-06-11 14:35:56 +02:00
Brian Clozel 76604db8da Stop observations for async requests in Servlet filter
Prior to this commit, the `ServerHttpObservationFilter` would support
async dispatches and would do the following:

1. start the observation
2. call the filter chain
3. if async has started, do nothing
4. if not in async mode, stop the observation

This behavior would effectively rely on Async implementations to
complete and dispatch the request back to the container for an async
dispatch. This is what Spring web frameworks do and guarantee.

Some implementations complete the async request but do not dispatch
back; as a result, observations could leak as they are never stopped.

This commit changes the support of async requests. The filter now
opts-out of async dispatches - the filter will not be called for those
anymore. Instead, if the application started async mode during the
initial container dispatch, the filter will register an AsyncListener to
be notified of the outcome of the async handling.

Fixes gh-32986
2024-06-07 19:00:26 +02:00
Sébastien Deleuze 38b7209dc5 Polishing
See gh-32984
2024-06-07 13:44:30 +02:00
Sébastien Deleuze df0764db5d Remove outdated copyright from index.adoc
Closes gh-32984
2024-06-07 13:38:25 +02:00
Sébastien Deleuze 1e2c4635a3 Exclude node_modules from NoHttp checks
Closes gh-32981
2024-06-07 11:49:43 +02:00
Juergen Hoeller 628b0504ec Skip ajc-compiled aspects for ajc-compiled target classes
Includes defensive ignoring of incompatible aspect types.

Closes gh-32970

(cherry picked from commit 0ea96b4806)
2024-06-06 21:01:42 +02:00
Juergen Hoeller 8b589db028 Avoid NoSuchMethodException for annotation attribute checks
Closes gh-32921

(cherry picked from commit b08883b65c)
2024-06-03 12:51:47 +02:00
Sam Brannen aed1d5f762 Support compilation of map indexing with primitive in SpEL
Prior to this commit, the Spring Expression Language (SpEL) failed to
compile an expression that indexed into a Map using a primitive literal
(boolean, int, long, float, or double).

This commit adds support for compilation of such expressions by
ensuring that primitive literals are boxed into their corresponding
wrapper types in the compiled bytecode.

Closes gh-32903

(cherry picked from commit e9de426eb5)
2024-05-28 10:39:54 +02:00
Sam Brannen 079d53c8d6 Support compilation of array and list indexing with Integer in SpEL
Prior to this commit, the Spring Expression Language (SpEL) failed to
compile an expression that indexed into an array or list using an
Integer.

This commit adds support for compilation of such expressions by
ensuring that an Integer is unboxed into an int in the compiled
bytecode.

See gh-32694
Closes gh-32908

(cherry picked from commit cda577d1aa)
2024-05-27 17:28:54 +02:00
Juergen Hoeller 206a89017c Test detection of original generic method for CGLIB bridge method
See gh-32888
2024-05-24 12:29:31 +02:00
Juergen Hoeller 8d1bf9607b Polishing
(cherry picked from commit 6c08d93992)
2024-05-23 17:12:22 +02:00
Juergen Hoeller e12440a7af Defensive handling of incompatible advice methods
This covers AspectJ transaction and caching aspects when encountered by Spring AOP.

Closes gh-32882
See gh-32793

(cherry picked from commit 6d7cd9c7dc)
2024-05-23 17:12:18 +02:00
Spring Builds 093e6a8e8d Next development version (v6.0.22-SNAPSHOT) 2024-05-22 16:07:26 +00:00
Stéphane Nicoll c993615f98 Remove outdated Javadoc links
Closes gh-32873
2024-05-22 14:49:07 +02:00
Rob Winch d068f5a4c6 Modernize Antora Build
- Use same playbook as docs-build
- Use Env Variables to cause partial build (same as docs-build)
- Use package.json so that dependencies can be updated with dependabot
2024-05-22 10:20:33 +02:00
Juergen Hoeller e73d68b0a8 Select most specific advice method in case of override
Closes gh-32865

(cherry picked from commit ea596aa211)
2024-05-22 10:07:31 +02:00
Juergen Hoeller 0ca47e5e03 Polishing 2024-05-21 18:24:04 +02:00
Juergen Hoeller 33d3496a16 Polishing
(cherry picked from commit 20dea0dae2)
2024-05-21 17:59:39 +02:00
Juergen Hoeller 84a5a8a61e Default fallback parsing for UTC without milliseconds
Closes gh-32856

(cherry picked from commit fee17e11ba)
2024-05-21 17:52:11 +02:00
Juergen Hoeller 2b8a1faeaa Polishing
(cherry picked from commit 65e1337d35)
2024-05-21 11:42:30 +02:00
Juergen Hoeller 4c9de3cbbd Avoid creation of SAXParserFactory for every read operation
Includes JAXBContext locking revision (avoiding synchronization) and consistent treatment of DocumentBuilderFactory (in terms of caching as well as locking).

Closes gh-32851

(cherry picked from commit a4c2f291d9)
2024-05-21 11:35:08 +02:00
Stéphane Nicoll a0f07af375 Avoid reader on empty content to be shared by multiple requests
This commit avoids several instances of MockHttpServletRequest to
have a common reader for empty content as closing it will have an
unwanted side effect on the others.

Closes gh-32848
2024-05-20 13:59:50 +02:00
Juergen Hoeller 97e12bd0e8 Defensively catch and log pointcut parsing exceptions
Closes gh-32838
See gh-32793

(cherry picked from commit 617833bec9)
2024-05-17 12:50:12 +02:00
Spring Builds c374c46cdc Next development version (v6.0.21-SNAPSHOT) 2024-05-16 08:24:45 +00:00
46 changed files with 1129 additions and 319 deletions
+2
View File
@@ -51,3 +51,5 @@ atlassian-ide-plugin.xml
.vscode/
cached-antora-playbook.yml
node_modules
+2 -4
View File
@@ -106,14 +106,12 @@ configure([rootProject] + javaProjects) { project ->
// JSR-305 only used for non-required meta-annotations
compileOnly("com.google.code.findbugs:jsr305")
testCompileOnly("com.google.code.findbugs:jsr305")
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.41")
checkstyle("io.spring.javaformat:spring-javaformat-checkstyle:0.0.42")
}
ext.javadocLinks = [
"https://docs.oracle.com/en/java/javase/17/docs/api/",
"https://jakarta.ee/specifications/platform/9/apidocs/",
"https://docs.oracle.com/cd/E13222_01/wls/docs90/javadocs/", // CommonJ and weblogic.* packages
"https://docs.jboss.org/jbossas/javadoc/4.0.5/connector/", // org.jboss.resource.*
"https://docs.jboss.org/hibernate/orm/5.6/javadocs/",
"https://eclipse.dev/aspectj/doc/released/aspectj5rt-api",
"https://www.quartz-scheduler.org/api/2.3.0/",
@@ -153,7 +151,7 @@ configure(rootProject) {
def rootPath = file(rootDir).toPath()
def projectDirs = allprojects.collect { it.projectDir } + "${rootDir}/buildSrc"
projectDirs.forEach { dir ->
[ 'bin', 'build', 'out', '.settings' ]
[ 'bin', 'build', 'out', '.settings', 'node_modules' ]
.collect { rootPath.relativize(new File(dir, it).toPath()) }
.forEach { source.exclude "$it/**" }
[ '.classpath', '.project' ]
+39
View File
@@ -0,0 +1,39 @@
antora:
extensions:
- require: '@springio/antora-extensions'
root_component_name: 'framework'
site:
title: Spring Framework
url: https://docs.spring.io/spring-framework/reference
robots: allow
git:
ensure_git_suffix: false
content:
sources:
- url: https://github.com/spring-projects/spring-framework
# Refname matching:
# https://docs.antora.org/antora/latest/playbook/content-refname-matching/
branches: ['main', '{6..9}.+({0..9}).x']
tags: ['v{6..9}.+({0..9}).+({0..9})?(-{RC,M}*)', '!(v6.0.{0..8})', '!(v6.0.0-{RC,M}{0..9})']
start_path: framework-docs
asciidoc:
extensions:
- '@asciidoctor/tabs'
- '@springio/asciidoctor-extensions'
- '@springio/asciidoctor-extensions/include-code-extension'
attributes:
page-stackoverflow-url: https://stackoverflow.com/questions/tagged/spring
page-pagination: ''
hide-uri-scheme: '@'
tabs-sync-option: '@'
include-java: 'example$docs-src/main/java/org/springframework/docs'
urls:
latest_version_segment_strategy: redirect:to
latest_version_segment: ''
redirect_facility: httpd
runtime:
log:
failure_level: warn
ui:
bundle:
url: https://github.com/spring-io/antora-ui-spring/releases/download/v0.4.15/ui-bundle.zip
+3 -20
View File
@@ -10,27 +10,10 @@ apply from: "${rootDir}/gradle/publications.gradle"
antora {
version = '3.2.0-alpha.2'
playbook = 'cached-antora-playbook.yml'
playbookProvider {
repository = 'spring-projects/spring-framework'
branch = 'docs-build'
path = 'lib/antora/templates/per-branch-antora-playbook.yml'
checkLocalBranch = true
}
options = ['--clean', '--stacktrace']
options = [clean: true, fetch: !project.gradle.startParameter.offline, stacktrace: true]
environment = [
'ALGOLIA_API_KEY': '82c7ead946afbac3cf98c32446154691',
'ALGOLIA_APP_ID': '244V8V9FGG',
'ALGOLIA_INDEX_NAME': 'framework-docs'
]
dependencies = [
'@antora/atlas-extension': '1.0.0-alpha.1',
'@antora/collector-extension': '1.0.0-alpha.3',
'@asciidoctor/tabs': '1.0.0-beta.3',
'@opendevise/antora-release-line-extension': '1.0.0-alpha.2',
'@springio/antora-extensions': '1.3.0',
'@springio/asciidoctor-extensions': '1.0.0-alpha.9'
'BUILD_REFNAME': 'HEAD',
'BUILD_VERSION': project.version,
]
}
@@ -153,15 +153,16 @@ Letting qualifier values select against target bean names, within the type-match
candidates, does not require a `@Qualifier` annotation at the injection point.
If there is no other resolution indicator (such as a qualifier or a primary marker),
for a non-unique dependency situation, Spring matches the injection point name
(that is, the field name or parameter name) against the target bean names and chooses the
same-named candidate, if any.
(that is, the field name or parameter name) against the target bean names and chooses
the same-named candidate, if any (either by bean name or by associated alias).
This requires the `-parameters` Java compiler flag to be present. As a fallback,
version 6.0 still supports parameter names from debug symbols via `-debug` as well.
====
That said, if you intend to express annotation-driven injection by name, do not
primarily use `@Autowired`, even if it is capable of selecting by bean name among
type-matching candidates. Instead, use the JSR-250 `@Resource` annotation, which is
semantically defined to identify a specific target component by its unique name, with
the declared type being irrelevant for the matching process. `@Autowired` has rather
As an alternative for injection by name, consider the JSR-250 `@Resource` annotation
which is semantically defined to identify a specific target component by its unique name,
with the declared type being irrelevant for the matching process. `@Autowired` has rather
different semantics: After selecting candidate beans by type, the specified `String`
qualifier value is considered within those type-selected candidates only (for example,
matching an `account` qualifier against beans marked with the same qualifier label).
@@ -416,8 +416,8 @@ Kotlin::
======
This approach shows that the factory bean itself can be managed and configured through
dependency injection (DI). See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail]
.
dependency injection (DI).
See xref:core/beans/dependencies/factory-properties-detailed.adoc[Dependencies and Configuration in Detail].
NOTE: In Spring documentation, "factory bean" refers to a bean that is configured in the
Spring container and that creates objects through an
@@ -444,5 +444,3 @@ cases into account and returns the type of object that a `BeanFactory.getBean` c
going to return for the same bean name.
+1 -3
View File
@@ -29,8 +29,6 @@ Brannen, Ramnivas Laddad, Arjen Poutsma, Chris Beams, Tareq Abedrabbo, Andy Clem
Syer, Oliver Gierke, Rossen Stoyanchev, Phillip Webb, Rob Winch, Brian Clozel, Stephane
Nicoll, Sebastien Deleuze, Jay Bryant, Mark Paluch
Copyright © 2002 - 2023 VMware, Inc. All Rights Reserved.
Copies of this document may be made for your own use and for distribution to others,
provided that you do not charge any fee for such copies and further provided that each
copy contains this Copyright Notice, whether distributed in print or electronically.
copy contains the Copyright Notice, whether distributed in print or electronically.
+10
View File
@@ -0,0 +1,10 @@
{
"dependencies": {
"antora": "3.2.0-alpha.4",
"@antora/atlas-extension": "1.0.0-alpha.2",
"@antora/collector-extension": "1.0.0-alpha.3",
"@asciidoctor/tabs": "1.0.0-beta.6",
"@springio/antora-extensions": "1.11.1",
"@springio/asciidoctor-extensions": "1.0.0-alpha.10"
}
}
+5 -5
View File
@@ -9,9 +9,9 @@ javaPlatform {
dependencies {
api(platform("com.fasterxml.jackson:jackson-bom:2.14.3"))
api(platform("io.micrometer:micrometer-bom:1.10.13"))
api(platform("io.netty:netty-bom:4.1.109.Final"))
api(platform("io.netty:netty-bom:4.1.111.Final"))
api(platform("io.netty:netty5-bom:5.0.0.Alpha5"))
api(platform("io.projectreactor:reactor-bom:2022.0.19"))
api(platform("io.projectreactor:reactor-bom:2022.0.20"))
api(platform("io.rsocket:rsocket-bom:1.1.3"))
api(platform("org.apache.groovy:groovy-bom:4.0.21"))
api(platform("org.apache.logging.log4j:log4j-bom:2.21.1"))
@@ -107,7 +107,7 @@ dependencies {
api("org.aspectj:aspectjtools:1.9.22.1")
api("org.aspectj:aspectjweaver:1.9.22.1")
api("org.assertj:assertj-core:3.24.2")
api("org.awaitility:awaitility:4.2.0")
api("org.awaitility:awaitility:4.2.1")
api("org.bouncycastle:bcpkix-jdk18on:1.72")
api("org.codehaus.jettison:jettison:1.5.4")
api("org.dom4j:dom4j:2.1.4")
@@ -126,7 +126,7 @@ dependencies {
api("org.hibernate:hibernate-validator:7.0.5.Final")
api("org.hsqldb:hsqldb:2.7.2")
api("org.javamoney:moneta:1.4.2")
api("org.jruby:jruby:9.4.6.0")
api("org.jruby:jruby:9.4.7.0")
api("org.junit.support:testng-engine:1.0.4")
api("org.mozilla:rhino:1.7.14")
api("org.ogce:xpp3:1.1.6")
@@ -135,7 +135,7 @@ dependencies {
api("org.seleniumhq.selenium:htmlunit-driver:2.70.0")
api("org.seleniumhq.selenium:selenium-java:3.141.59")
api("org.skyscreamer:jsonassert:1.5.1")
api("org.slf4j:slf4j-api:2.0.12")
api("org.slf4j:slf4j-api:2.0.13")
api("org.testng:testng:7.8.0")
api("org.webjars:underscorejs:1.8.3")
api("org.webjars:webjars-locator-core:0.55")
+1 -1
View File
@@ -1,4 +1,4 @@
version=6.0.20-SNAPSHOT
version=6.0.22
org.gradle.caching=true
org.gradle.jvmargs=-Xmx2048m
@@ -63,7 +63,7 @@ class EnableCachingIntegrationTests {
// attempt was made to look up the AJ aspect. It's due to classpath issues
// in integration-tests that it's not found.
assertThatException().isThrownBy(ctx::refresh)
.withMessageContaining("AspectJCachingConfiguration");
.withMessageContaining("AspectJCachingConfiguration");
}
@@ -542,8 +542,7 @@ public class EnvironmentSystemIntegrationTests {
{
ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setRequiredProperties("foo", "bar");
assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(
ctx::refresh);
assertThatExceptionOfType(MissingRequiredPropertiesException.class).isThrownBy(ctx::refresh);
}
{
@@ -98,9 +98,8 @@ class EnableTransactionManagementIntegrationTests {
// this test is a bit fragile, but gets the job done, proving that an
// attempt was made to look up the AJ aspect. It's due to classpath issues
// in integration-tests that it's not found.
assertThatException()
.isThrownBy(ctx::refresh)
.withMessageContaining("AspectJJtaTransactionManagementConfiguration");
assertThatException().isThrownBy(ctx::refresh)
.withMessageContaining("AspectJJtaTransactionManagementConfiguration");
}
@Test
@@ -18,6 +18,7 @@ package org.springframework.aop.aspectj;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
@@ -41,6 +42,7 @@ import org.aspectj.weaver.tools.PointcutParameter;
import org.aspectj.weaver.tools.PointcutParser;
import org.aspectj.weaver.tools.PointcutPrimitive;
import org.aspectj.weaver.tools.ShadowMatch;
import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException;
import org.springframework.aop.ClassFilter;
import org.springframework.aop.IntroductionAwareMethodMatcher;
@@ -84,6 +86,8 @@ import org.springframework.util.StringUtils;
public class AspectJExpressionPointcut extends AbstractExpressionPointcut
implements ClassFilter, IntroductionAwareMethodMatcher, BeanFactoryAware {
private static final String AJC_MAGIC = "ajc$";
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = Set.of(
PointcutPrimitive.EXECUTION,
PointcutPrimitive.ARGS,
@@ -101,6 +105,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Nullable
private Class<?> pointcutDeclarationScope;
private boolean aspectCompiledByAjc;
private String[] pointcutParameterNames = new String[0];
private Class<?>[] pointcutParameterTypes = new Class<?>[0];
@@ -114,6 +120,8 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Nullable
private transient PointcutExpression pointcutExpression;
private transient boolean pointcutParsingFailed = false;
private transient Map<Method, ShadowMatch> shadowMatchCache = new ConcurrentHashMap<>(32);
@@ -130,7 +138,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
* @param paramTypes the parameter types for the pointcut
*/
public AspectJExpressionPointcut(Class<?> declarationScope, String[] paramNames, Class<?>[] paramTypes) {
this.pointcutDeclarationScope = declarationScope;
setPointcutDeclarationScope(declarationScope);
if (paramNames.length != paramTypes.length) {
throw new IllegalStateException(
"Number of pointcut parameter names must match number of pointcut parameter types");
@@ -145,6 +153,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
*/
public void setPointcutDeclarationScope(Class<?> pointcutDeclarationScope) {
this.pointcutDeclarationScope = pointcutDeclarationScope;
this.aspectCompiledByAjc = compiledByAjc(pointcutDeclarationScope);
}
/**
@@ -269,6 +278,15 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
@Override
public boolean matches(Class<?> targetClass) {
if (this.pointcutParsingFailed) {
// Pointcut parsing failed before below -> avoid trying again.
return false;
}
if (this.aspectCompiledByAjc && compiledByAjc(targetClass)) {
// ajc-compiled aspect class for ajc-compiled target class -> already weaved.
return false;
}
try {
try {
return obtainPointcutExpression().couldMatchJoinPointsInType(targetClass);
@@ -282,8 +300,11 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
}
}
}
catch (IllegalArgumentException | IllegalStateException ex) {
throw ex;
catch (IllegalArgumentException | IllegalStateException | UnsupportedPointcutPrimitiveException ex) {
this.pointcutParsingFailed = true;
if (logger.isDebugEnabled()) {
logger.debug("Pointcut parser rejected expression [" + getExpression() + "]: " + ex);
}
}
catch (Throwable ex) {
logger.debug("PointcutExpression matching rejected target class", ex);
@@ -518,6 +539,15 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut
return shadowMatch;
}
private static boolean compiledByAjc(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().startsWith(AJC_MAGIC)) {
return true;
}
}
return false;
}
@Override
public boolean equals(@Nullable Object other) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,9 +22,12 @@ import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.aspectj.lang.reflect.PerClauseKind;
import org.springframework.aop.Advisor;
import org.springframework.aop.framework.AopConfigException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.lang.Nullable;
@@ -40,6 +43,8 @@ import org.springframework.util.Assert;
*/
public class BeanFactoryAspectJAdvisorsBuilder {
private static final Log logger = LogFactory.getLog(BeanFactoryAspectJAdvisorsBuilder.class);
private final ListableBeanFactory beanFactory;
private final AspectJAdvisorFactory advisorFactory;
@@ -102,30 +107,37 @@ public class BeanFactoryAspectJAdvisorsBuilder {
continue;
}
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
try {
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
advisors.addAll(classAdvisors);
aspectNames.add(beanName);
}
else {
// Per target or per this.
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
catch (IllegalArgumentException | IllegalStateException | AopConfigException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring incompatible aspect [" + beanType.getName() + "]: " + ex);
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -50,6 +50,7 @@ import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConvertingComparator;
import org.springframework.lang.Nullable;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodFilter;
import org.springframework.util.StringUtils;
@@ -133,17 +134,19 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
List<Advisor> advisors = new ArrayList<>();
for (Method method : getAdvisorMethods(aspectClass)) {
// Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect
// to getAdvisor(...) to represent the "current position" in the declared methods list.
// However, since Java 7 the "current position" is not valid since the JDK no longer
// returns declared methods in the order in which they are declared in the source code.
// Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods
// discovered via reflection in order to support reliable advice ordering across JVM launches.
// Specifically, a value of 0 aligns with the default value used in
// AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor).
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
if (advisor != null) {
advisors.add(advisor);
if (method.equals(ClassUtils.getMostSpecificMethod(method, aspectClass))) {
// Prior to Spring Framework 5.2.7, advisors.size() was supplied as the declarationOrderInAspect
// to getAdvisor(...) to represent the "current position" in the declared methods list.
// However, since Java 7 the "current position" is not valid since the JDK no longer
// returns declared methods in the order in which they are declared in the source code.
// Thus, we now hard code the declarationOrderInAspect to 0 for all advice methods
// discovered via reflection in order to support reliable advice ordering across JVM launches.
// Specifically, a value of 0 aligns with the default value used in
// AspectJPrecedenceComparator.getAspectDeclarationOrder(Advisor).
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 0, aspectName);
if (advisor != null) {
advisors.add(advisor);
}
}
}
@@ -210,8 +213,16 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto
return null;
}
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
try {
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}
catch (IllegalArgumentException | IllegalStateException ex) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring incompatible advice method: " + candidateAdviceMethod, ex);
}
return null;
}
}
@Nullable
@@ -39,13 +39,13 @@ import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.beans.testfixture.beans.subpkg.DeepBean;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* @author Rob Harrop
* @author Rod Johnson
* @author Chris Beams
* @author Juergen Hoeller
*/
public class AspectJExpressionPointcutTests {
@@ -244,7 +244,7 @@ public class AspectJExpressionPointcutTests {
@Test
public void testInvalidExpression() {
String expression = "execution(void org.springframework.beans.testfixture.beans.TestBean.setSomeNumber(Number) && args(Double)";
assertThatIllegalArgumentException().isThrownBy(() -> getPointcut(expression).getClassFilter().matches(Object.class));
assertThat(getPointcut(expression).getClassFilter().matches(Object.class)).isFalse();
}
private TestBean getAdvisedProxy(String pointcutExpression, CallCountingInterceptor interceptor) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -38,7 +38,6 @@ import org.aspectj.lang.annotation.DeclareParents;
import org.aspectj.lang.annotation.DeclarePrecedence;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.aop.Advisor;
@@ -84,15 +83,15 @@ abstract class AbstractAspectJAdvisorFactoryTests {
@Test
void rejectsPerCflowAspect() {
assertThatExceptionOfType(AopConfigException.class)
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean")))
.withMessageContaining("PERCFLOW");
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowAspect(), "someBean")))
.withMessageContaining("PERCFLOW");
}
@Test
void rejectsPerCflowBelowAspect() {
assertThatExceptionOfType(AopConfigException.class)
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean")))
.withMessageContaining("PERCFLOWBELOW");
.isThrownBy(() -> getAdvisorFactory().getAdvisors(aspectInstanceFactory(new PerCflowBelowAspect(), "someBean")))
.withMessageContaining("PERCFLOWBELOW");
}
@Test
@@ -363,7 +362,7 @@ abstract class AbstractAspectJAdvisorFactoryTests {
assertThat(lockable.locked()).as("Already locked").isTrue();
lockable.lock();
assertThat(lockable.locked()).as("Real target ignores locking").isTrue();
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> lockable.unlock());
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(lockable::unlock);
}
@Test
@@ -389,9 +388,7 @@ abstract class AbstractAspectJAdvisorFactoryTests {
assertThat(lockable.locked()).isTrue();
}
// TODO: Why does this test fail? It hasn't been run before, so it maybe never actually passed...
@Test
@Disabled
void introductionWithArgumentBinding() {
TestBean target = new TestBean();
@@ -648,7 +645,7 @@ abstract class AbstractAspectJAdvisorFactoryTests {
static class NamedPointcutAspectWithFQN {
@SuppressWarnings("unused")
private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean();
private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean();
@Around("org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactoryTests.CommonPointcuts.getAge()()")
int changeReturnValue(ProceedingJoinPoint pjp) {
@@ -765,7 +762,7 @@ abstract class AbstractAspectJAdvisorFactoryTests {
@Aspect
class DoublingAspect {
static class DoublingAspect {
@Around("execution(* getAge())")
public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable {
@@ -773,8 +770,14 @@ abstract class AbstractAspectJAdvisorFactoryTests {
}
}
@Aspect
class IncrementingAspect extends DoublingAspect {
static class IncrementingAspect extends DoublingAspect {
@Override
public Object doubleAge(ProceedingJoinPoint pjp) throws Throwable {
return ((int) pjp.proceed()) * 2;
}
@Around("execution(* getAge())")
public int incrementAge(ProceedingJoinPoint pjp) throws Throwable {
@@ -783,7 +786,6 @@ abstract class AbstractAspectJAdvisorFactoryTests {
}
@Aspect
private static class InvocationTrackingAspect {
@@ -1083,7 +1085,7 @@ class PerThisAspect {
// Just to check that this doesn't cause problems with introduction processing
@SuppressWarnings("unused")
private ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean();
private final ITestBean fieldThatShouldBeIgnoredBySpringAtAspectJProcessing = new TestBean();
@Around("execution(int *.getAge())")
int returnCountAsAge() {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,12 +22,13 @@ import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Adrian Colyer
* @author Juergen Hoeller
*/
public class AutoProxyWithCodeStyleAspectsTests {
@Test
@SuppressWarnings("resource")
public void noAutoproxyingOfAjcCompiledAspects() {
public void noAutoProxyingOfAjcCompiledAspects() {
new ClassPathXmlApplicationContext("org/springframework/aop/aspectj/autoproxy/ajcAutoproxyTests.xml");
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,10 @@ import org.junit.jupiter.api.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
/**
* @author Ramnivas Laddad
* @author Juergen Hoeller
*/
public class SpringConfiguredWithAutoProxyingTests {
@Test
@@ -2,16 +2,29 @@
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop-2.0.xsd
http://www.springframework.org/schema/cache https://www.springframework.org/schema/cache/spring-cache-3.1.xsd
http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-2.5.xsd">
<aop:aspectj-autoproxy/>
<bean id="myAspect" class="org.springframework.aop.aspectj.autoproxy.CodeStyleAspect"
factory-method="aspectOf">
<context:spring-configured/>
<cache:annotation-driven mode="aspectj"/>
<bean id="cacheManager" class="org.springframework.cache.support.NoOpCacheManager"/>
<bean id="myAspect" class="org.springframework.aop.aspectj.autoproxy.CodeStyleAspect" factory-method="aspectOf">
<property name="foo" value="bar"/>
</bean>
<bean id="otherBean" class="java.lang.Object"/>
<bean id="otherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
<bean id="yetAnotherBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring"/>
<bean id="configuredBean" class="org.springframework.beans.factory.aspectj.ShouldBeConfiguredBySpring" lazy-init="true"/>
</beans>
@@ -24,8 +24,7 @@
</property>
</bean>
<bean id="defaultCache"
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<bean id="defaultCache" class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="default"/>
</bean>
@@ -7,12 +7,10 @@
http://www.springframework.org/schema/task
https://www.springframework.org/schema/task/spring-task.xsd">
<task:annotation-driven mode="aspectj" executor="testExecutor"
exception-handler="testExceptionHandler"/>
<task:annotation-driven mode="aspectj" executor="testExecutor" exception-handler="testExceptionHandler"/>
<task:executor id="testExecutor"/>
<bean id="testExceptionHandler"
class="org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"/>
<bean id="testExceptionHandler" class="org.springframework.aop.interceptor.SimpleAsyncUncaughtExceptionHandler"/>
</beans>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -41,6 +41,8 @@ import org.springframework.beans.propertyeditors.CustomDateEditor;
import org.springframework.beans.testfixture.beans.DerivedTestBean;
import org.springframework.beans.testfixture.beans.ITestBean;
import org.springframework.beans.testfixture.beans.TestBean;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceEditor;
import org.springframework.lang.Nullable;
@@ -51,7 +53,7 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
import static org.assertj.core.api.SoftAssertions.assertSoftly;
/**
* Unit tests for {@link BeanUtils}.
* Tests for {@link BeanUtils}.
*
* @author Juergen Hoeller
* @author Rob Harrop
@@ -321,12 +323,13 @@ class BeanUtilsTests {
Order original = new Order("test", List.of("foo", "bar"));
// Create a Proxy that loses the generic type information for the getLineItems() method.
OrderSummary proxy = proxyOrder(original);
OrderSummary proxy = (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] {OrderSummary.class}, new OrderInvocationHandler(original));
assertThat(OrderSummary.class.getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List<java.lang.String>");
.contains("java.util.List<java.lang.String>");
assertThat(proxy.getClass().getDeclaredMethod("getLineItems").toGenericString())
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
.contains("java.util.List")
.doesNotContain("<java.lang.String>");
// Ensure that our custom Proxy works as expected.
assertThat(proxy.getId()).isEqualTo("test");
@@ -339,6 +342,23 @@ class BeanUtilsTests {
assertThat(target.getLineItems()).containsExactly("foo", "bar");
}
@Test // gh-32888
public void copyPropertiesWithGenericCglibClass() {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(User.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> proxy.invokeSuper(obj, args));
User user = (User) enhancer.create();
user.setId(1);
user.setName("proxy");
user.setAddress("addr");
User target = new User();
BeanUtils.copyProperties(user, target);
assertThat(target.getId()).isEqualTo(user.getId());
assertThat(target.getName()).isEqualTo(user.getName());
assertThat(target.getAddress()).isEqualTo(user.getAddress());
}
@Test
void copyPropertiesWithEditable() throws Exception {
TestBean tb = new TestBean();
@@ -518,6 +538,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerHolder {
@@ -532,6 +553,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class WildcardListHolder1 {
@@ -546,6 +568,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class WildcardListHolder2 {
@@ -560,6 +583,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class NumberUpperBoundedWildcardListHolder {
@@ -574,6 +598,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class NumberListHolder {
@@ -588,6 +613,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerListHolder1 {
@@ -602,6 +628,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class IntegerListHolder2 {
@@ -616,6 +643,7 @@ class BeanUtilsTests {
}
}
@SuppressWarnings("unused")
private static class LongListHolder {
@@ -796,6 +824,7 @@ class BeanUtilsTests {
}
}
private static class BeanWithNullableTypes {
private Integer counter;
@@ -826,6 +855,7 @@ class BeanUtilsTests {
}
}
private static class BeanWithPrimitiveTypes {
private boolean flag;
@@ -838,7 +868,6 @@ class BeanUtilsTests {
private char character;
private String text;
@SuppressWarnings("unused")
public BeanWithPrimitiveTypes(boolean flag, byte byteCount, short shortCount, int intCount, long longCount,
float floatCount, double doubleCount, char character, String text) {
@@ -889,21 +918,22 @@ class BeanUtilsTests {
public String getText() {
return text;
}
}
private static class PrivateBeanWithPrivateConstructor {
private PrivateBeanWithPrivateConstructor() {
}
}
@SuppressWarnings("unused")
private static class Order {
private String id;
private List<String> lineItems;
private List<String> lineItems;
Order() {
}
@@ -935,6 +965,7 @@ class BeanUtilsTests {
}
}
private interface OrderSummary {
String getId();
@@ -943,17 +974,10 @@ class BeanUtilsTests {
}
private OrderSummary proxyOrder(Order order) {
return (OrderSummary) Proxy.newProxyInstance(getClass().getClassLoader(),
new Class<?>[] { OrderSummary.class }, new OrderInvocationHandler(order));
}
private static class OrderInvocationHandler implements InvocationHandler {
private final Order order;
OrderInvocationHandler(Order order) {
this.order = order;
}
@@ -971,4 +995,46 @@ class BeanUtilsTests {
}
}
private static class GenericBaseModel<T> {
private T id;
private String name;
public T getId() {
return id;
}
public void setId(T id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
private static class User extends GenericBaseModel<Integer> {
private String address;
public User() {
super();
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
}
@@ -149,13 +149,13 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
lifecycleBeans.forEach((beanName, bean) -> {
if (!autoStartupOnly || (bean instanceof SmartLifecycle smartLifecycle && smartLifecycle.isAutoStartup())) {
int phase = getPhase(bean);
phases.computeIfAbsent(
phase,
p -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
int startupPhase = getPhase(bean);
phases.computeIfAbsent(startupPhase,
phase -> new LifecycleGroup(phase, this.timeoutPerShutdownPhase, lifecycleBeans, autoStartupOnly)
).add(beanName, bean);
}
});
if (!phases.isEmpty()) {
phases.values().forEach(LifecycleGroup::start);
}
@@ -195,6 +195,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
private void stopBeans() {
Map<String, Lifecycle> lifecycleBeans = getLifecycleBeans();
Map<Integer, LifecycleGroup> phases = new HashMap<>();
lifecycleBeans.forEach((beanName, bean) -> {
int shutdownPhase = getPhase(bean);
LifecycleGroup group = phases.get(shutdownPhase);
@@ -204,6 +205,7 @@ public class DefaultLifecycleProcessor implements LifecycleProcessor, BeanFactor
}
group.add(beanName, bean);
});
if (!phases.isEmpty()) {
List<Integer> keys = new ArrayList<>(phases.keySet());
keys.sort(Collections.reverseOrder());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -22,8 +22,10 @@ import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TimeZone;
import org.springframework.format.Formatter;
@@ -35,9 +37,14 @@ import org.springframework.util.StringUtils;
/**
* A formatter for {@link java.util.Date} types.
*
* <p>Supports the configuration of an explicit date time pattern, timezone,
* locale, and fallback date time patterns for lenient parsing.
*
* <p>Common ISO patterns for UTC instants are applied at millisecond precision.
* Note that {@link org.springframework.format.datetime.standard.InstantFormatter}
* is recommended for flexible UTC parsing into a {@link java.time.Instant} instead.
*
* @author Keith Donald
* @author Juergen Hoeller
* @author Phillip Webb
@@ -49,15 +56,23 @@ public class DateFormatter implements Formatter<Date> {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
private static final Map<ISO, String> ISO_PATTERNS;
private static final Map<ISO, String> ISO_FALLBACK_PATTERNS;
static {
// We use an EnumMap instead of Map.of(...) since the former provides better performance.
Map<ISO, String> formats = new EnumMap<>(ISO.class);
formats.put(ISO.DATE, "yyyy-MM-dd");
formats.put(ISO.TIME, "HH:mm:ss.SSSXXX");
formats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
ISO_PATTERNS = Collections.unmodifiableMap(formats);
// Fallback format for the time part without milliseconds.
Map<ISO, String> fallbackFormats = new EnumMap<>(ISO.class);
fallbackFormats.put(ISO.TIME, "HH:mm:ssXXX");
fallbackFormats.put(ISO.DATE_TIME, "yyyy-MM-dd'T'HH:mm:ssXXX");
ISO_FALLBACK_PATTERNS = Collections.unmodifiableMap(fallbackFormats);
}
@@ -202,8 +217,16 @@ public class DateFormatter implements Formatter<Date> {
return getDateFormat(locale).parse(text);
}
catch (ParseException ex) {
Set<String> fallbackPatterns = new LinkedHashSet<>();
String isoPattern = ISO_FALLBACK_PATTERNS.get(this.iso);
if (isoPattern != null) {
fallbackPatterns.add(isoPattern);
}
if (!ObjectUtils.isEmpty(this.fallbackPatterns)) {
for (String pattern : this.fallbackPatterns) {
Collections.addAll(fallbackPatterns, this.fallbackPatterns);
}
if (!fallbackPatterns.isEmpty()) {
for (String pattern : fallbackPatterns) {
try {
DateFormat dateFormat = configureDateFormat(new SimpleDateFormat(pattern, locale));
// Align timezone for parsing format with printing format if ISO is set.
@@ -221,8 +244,8 @@ public class DateFormatter implements Formatter<Date> {
}
if (this.source != null) {
ParseException parseException = new ParseException(
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
ex.getErrorOffset());
String.format("Unable to parse date time value \"%s\" using configuration from %s", text, this.source),
ex.getErrorOffset());
parseException.initCause(ex);
throw parseException;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 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.
@@ -41,12 +41,12 @@ public class InstantFormatter implements Formatter<Instant> {
@Override
public Instant parse(String text, Locale locale) throws ParseException {
if (text.length() > 0 && Character.isAlphabetic(text.charAt(0))) {
if (!text.isEmpty() && Character.isAlphabetic(text.charAt(0))) {
// assuming RFC-1123 value a la "Tue, 3 Jun 2008 11:05:30 GMT"
return Instant.from(DateTimeFormatter.RFC_1123_DATE_TIME.parse(text));
}
else {
// assuming UTC instant a la "2007-12-03T10:15:30.00Z"
// assuming UTC instant a la "2007-12-03T10:15:30.000Z"
return Instant.parse(text);
}
}
@@ -28,17 +28,14 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Adrian Colyer
* @author Chris Beams
* @author Juergen Hoeller
*/
class OverloadedAdviceTests {
@Test
@SuppressWarnings("resource")
void testExceptionOnConfigParsingWithMismatchedAdviceMethod() {
assertThatExceptionOfType(BeanCreationException.class)
.isThrownBy(() -> new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass()))
.havingRootCause()
.isInstanceOf(IllegalArgumentException.class)
.as("invalidAbsoluteTypeName should be detected by AJ").withMessageContaining("invalidAbsoluteTypeName");
void testConfigParsingWithMismatchedAdviceMethod() {
new ClassPathXmlApplicationContext(getClass().getSimpleName() + ".xml", getClass());
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -35,75 +35,83 @@ import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
*
* @author Keith Donald
* @author Phillip Webb
* @author Juergen Hoeller
*/
public class DateFormatterTests {
class DateFormatterTests {
private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
@Test
public void shouldPrintAndParseDefault() throws Exception {
void shouldPrintAndParseDefault() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
}
@Test
public void shouldPrintAndParseFromPattern() throws ParseException {
void shouldPrintAndParseFromPattern() throws ParseException {
DateFormatter formatter = new DateFormatter("yyyy-MM-dd");
formatter.setTimeZone(UTC);
Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
assertThat(formatter.parse("2009-06-01", Locale.US)).isEqualTo(date);
}
@Test
public void shouldPrintAndParseShort() throws Exception {
void shouldPrintAndParseShort() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.SHORT);
Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("6/1/09");
assertThat(formatter.parse("6/1/09", Locale.US)).isEqualTo(date);
}
@Test
public void shouldPrintAndParseMedium() throws Exception {
void shouldPrintAndParseMedium() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.MEDIUM);
Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("Jun 1, 2009");
assertThat(formatter.parse("Jun 1, 2009", Locale.US)).isEqualTo(date);
}
@Test
public void shouldPrintAndParseLong() throws Exception {
void shouldPrintAndParseLong() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.LONG);
Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("June 1, 2009");
assertThat(formatter.parse("June 1, 2009", Locale.US)).isEqualTo(date);
}
@Test
public void shouldPrintAndParseFull() throws Exception {
void shouldPrintAndParseFull() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.FULL);
Date date = getDate(2009, Calendar.JUNE, 1);
assertThat(formatter.print(date, Locale.US)).isEqualTo("Monday, June 1, 2009");
assertThat(formatter.parse("Monday, June 1, 2009", Locale.US)).isEqualTo(date);
}
@Test
public void shouldPrintAndParseISODate() throws Exception {
void shouldPrintAndParseIsoDate() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setIso(ISO.DATE);
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01");
assertThat(formatter.parse("2009-6-01", Locale.US))
@@ -111,45 +119,56 @@ public class DateFormatterTests {
}
@Test
public void shouldPrintAndParseISOTime() throws Exception {
void shouldPrintAndParseIsoTime() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setIso(ISO.TIME);
Date date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 3);
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.003Z");
assertThat(formatter.parse("14:23:05.003Z", Locale.US))
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 3));
date = getDate(2009, Calendar.JANUARY, 1, 14, 23, 5, 0);
assertThat(formatter.print(date, Locale.US)).isEqualTo("14:23:05.000Z");
assertThat(formatter.parse("14:23:05Z", Locale.US))
.isEqualTo(getDate(1970, Calendar.JANUARY, 1, 14, 23, 5, 0).toInstant());
}
@Test
public void shouldPrintAndParseISODateTime() throws Exception {
void shouldPrintAndParseIsoDateTime() throws Exception {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setIso(ISO.DATE_TIME);
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.003Z");
assertThat(formatter.parse("2009-06-01T14:23:05.003Z", Locale.US)).isEqualTo(date);
date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 0);
assertThat(formatter.print(date, Locale.US)).isEqualTo("2009-06-01T14:23:05.000Z");
assertThat(formatter.parse("2009-06-01T14:23:05Z", Locale.US)).isEqualTo(date.toInstant());
}
@Test
public void shouldThrowOnUnsupportedStylePattern() throws Exception {
void shouldThrowOnUnsupportedStylePattern() {
DateFormatter formatter = new DateFormatter();
formatter.setStylePattern("OO");
assertThatIllegalStateException().isThrownBy(() ->
formatter.parse("2009", Locale.US))
.withMessageContaining("Unsupported style pattern 'OO'");
assertThatIllegalStateException().isThrownBy(() -> formatter.parse("2009", Locale.US))
.withMessageContaining("Unsupported style pattern 'OO'");
}
@Test
public void shouldUseCorrectOrder() throws Exception {
void shouldUseCorrectOrder() {
DateFormatter formatter = new DateFormatter();
formatter.setTimeZone(UTC);
formatter.setStyle(DateFormat.SHORT);
formatter.setStylePattern("L-");
formatter.setIso(ISO.DATE_TIME);
formatter.setPattern("yyyy");
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
Date date = getDate(2009, Calendar.JUNE, 1, 14, 23, 5, 3);
assertThat(formatter.print(date, Locale.US)).as("uses pattern").isEqualTo("2009");
formatter.setPattern("");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -52,7 +52,7 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Juergen Hoeller
* @author Sam Brannen
*/
public class DateFormattingTests {
class DateFormattingTests {
private final FormattingConversionService conversionService = new FormattingConversionService();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -274,9 +274,7 @@ class DateTimeFormattingTests {
binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isZero();
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
assertThat(value)
.startsWith("10/31/09")
.endsWith("12:00 PM");
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
}
@Test
@@ -286,9 +284,7 @@ class DateTimeFormattingTests {
binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isZero();
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
assertThat(value)
.startsWith("10/31/09")
.endsWith("12:00 PM");
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
}
@Test
@@ -298,9 +294,7 @@ class DateTimeFormattingTests {
binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isZero();
String value = binder.getBindingResult().getFieldValue("styleLocalDateTime").toString();
assertThat(value)
.startsWith("Oct 31, 2009")
.endsWith("12:00:00 PM");
assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM");
}
@Test
@@ -310,9 +304,7 @@ class DateTimeFormattingTests {
binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isZero();
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
assertThat(value)
.startsWith("10/31/09")
.endsWith("12:00 PM");
assertThat(value).startsWith("10/31/09").endsWith("12:00 PM");
}
@Test
@@ -325,9 +317,7 @@ class DateTimeFormattingTests {
binder.bind(propertyValues);
assertThat(binder.getBindingResult().getErrorCount()).isZero();
String value = binder.getBindingResult().getFieldValue("localDateTime").toString();
assertThat(value)
.startsWith("Oct 31, 2009")
.endsWith("12:00:00 PM");
assertThat(value).startsWith("Oct 31, 2009").endsWith("12:00:00 PM");
}
@Test
@@ -512,7 +502,7 @@ class DateTimeFormattingTests {
}
@Test
public void testBindYearMonthAnnotatedPattern() {
void testBindYearMonthAnnotatedPattern() {
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("yearMonthAnnotatedPattern", "12/2007");
binder.bind(propertyValues);
@@ -531,7 +521,7 @@ class DateTimeFormattingTests {
}
@Test
public void testBindMonthDayAnnotatedPattern() {
void testBindMonthDayAnnotatedPattern() {
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.add("monthDayAnnotatedPattern", "1/3");
binder.bind(propertyValues);
@@ -540,6 +530,7 @@ class DateTimeFormattingTests {
assertThat(binder.getBindingResult().getRawFieldValue("monthDayAnnotatedPattern")).isEqualTo(MonthDay.parse("--01-03"));
}
@Nested
class FallbackPatternTests {
@@ -631,10 +622,10 @@ class DateTimeFormattingTests {
@DateTimeFormat(style = "M-")
private LocalDate styleLocalDate;
@DateTimeFormat(style = "S-", fallbackPatterns = { "yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd" })
@DateTimeFormat(style = "S-", fallbackPatterns = {"yyyy-MM-dd", "yyyyMMdd", "yyyy.MM.dd"})
private LocalDate styleLocalDateWithFallbackPatterns;
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = { "M/d/yy", "yyyyMMdd", "yyyy.MM.dd" })
@DateTimeFormat(pattern = "yyyy-MM-dd", fallbackPatterns = {"M/d/yy", "yyyyMMdd", "yyyy.MM.dd"})
private LocalDate patternLocalDateWithFallbackPatterns;
private LocalTime localTime;
@@ -642,7 +633,7 @@ class DateTimeFormattingTests {
@DateTimeFormat(style = "-M")
private LocalTime styleLocalTime;
@DateTimeFormat(style = "-M", fallbackPatterns = { "HH:mm:ss", "HH:mm"})
@DateTimeFormat(style = "-M", fallbackPatterns = {"HH:mm:ss", "HH:mm"})
private LocalTime styleLocalTimeWithFallbackPatterns;
private LocalDateTime localDateTime;
@@ -662,7 +653,7 @@ class DateTimeFormattingTests {
@DateTimeFormat(iso = ISO.DATE_TIME)
private LocalDateTime isoLocalDateTime;
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = { "yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
@DateTimeFormat(iso = ISO.DATE_TIME, fallbackPatterns = {"yyyy-MM-dd HH:mm:ss", "M/d/yy HH:mm"})
private LocalDateTime isoLocalDateTimeWithFallbackPatterns;
private Instant instant;
@@ -690,7 +681,6 @@ class DateTimeFormattingTests {
private final List<DateTimeBean> children = new ArrayList<>();
public LocalDate getLocalDate() {
return this.localDate;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.format.datetime.standard;
import java.text.ParseException;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.Random;
import java.util.stream.Stream;
@@ -49,13 +50,12 @@ class InstantFormatterTests {
private final InstantFormatter instantFormatter = new InstantFormatter();
@ParameterizedTest
@ArgumentsSource(ISOSerializedInstantProvider.class)
void should_parse_an_ISO_formatted_string_representation_of_an_Instant(String input) throws ParseException {
Instant expected = DateTimeFormatter.ISO_INSTANT.parse(input, Instant::from);
Instant actual = instantFormatter.parse(input, null);
Instant actual = instantFormatter.parse(input, Locale.US);
assertThat(actual).isEqualTo(expected);
}
@@ -63,9 +63,7 @@ class InstantFormatterTests {
@ArgumentsSource(RFC1123SerializedInstantProvider.class)
void should_parse_an_RFC1123_formatted_string_representation_of_an_Instant(String input) throws ParseException {
Instant expected = DateTimeFormatter.RFC_1123_DATE_TIME.parse(input, Instant::from);
Instant actual = instantFormatter.parse(input, null);
Instant actual = instantFormatter.parse(input, Locale.US);
assertThat(actual).isEqualTo(expected);
}
@@ -73,12 +71,11 @@ class InstantFormatterTests {
@ArgumentsSource(RandomInstantProvider.class)
void should_serialize_an_Instant_using_ISO_format_and_ignoring_Locale(Instant input) {
String expected = DateTimeFormatter.ISO_INSTANT.format(input);
String actual = instantFormatter.print(input, null);
String actual = instantFormatter.print(input, Locale.US);
assertThat(actual).isEqualTo(expected);
}
private static class RandomInstantProvider implements ArgumentsProvider {
private static final long DATA_SET_SIZE = 10;
@@ -100,6 +97,7 @@ class InstantFormatterTests {
}
}
private static class ISOSerializedInstantProvider extends RandomInstantProvider {
@Override
@@ -108,6 +106,7 @@ class InstantFormatterTests {
}
}
private static class RFC1123SerializedInstantProvider extends RandomInstantProvider {
// RFC-1123 supports only 4-digit years
@@ -18,4 +18,6 @@
<bean id="testBean" class="org.springframework.beans.testfixture.beans.TestBean"/>
<bean id="testBean2" class="org.springframework.beans.testfixture.beans.TestBean"/>
</beans>
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -57,11 +57,11 @@ public final class BridgeMethodResolver {
/**
* Find the original method for the supplied {@link Method bridge Method}.
* Find the local original method for the supplied {@link Method bridge Method}.
* <p>It is safe to call this method passing in a non-bridge {@link Method} instance.
* In such a case, the supplied {@link Method} instance is returned directly to the caller.
* Callers are <strong>not</strong> required to check for bridging before calling this method.
* @param bridgeMethod the method to introspect
* @param bridgeMethod the method to introspect against its declaring class
* @return the original method (either the bridged method or the passed-in method
* if no more specific one could be found)
*/
@@ -73,8 +73,7 @@ public final class BridgeMethodResolver {
if (bridgedMethod == null) {
// Gather all methods with matching name and parameter size.
List<Method> candidateMethods = new ArrayList<>();
MethodFilter filter = candidateMethod ->
isBridgedCandidateFor(candidateMethod, bridgeMethod);
MethodFilter filter = (candidateMethod -> isBridgedCandidateFor(candidateMethod, bridgeMethod));
ReflectionUtils.doWithMethods(bridgeMethod.getDeclaringClass(), candidateMethods::add, filter);
if (!candidateMethods.isEmpty()) {
bridgedMethod = candidateMethods.size() == 1 ?
@@ -121,8 +120,8 @@ public final class BridgeMethodResolver {
return candidateMethod;
}
else if (previousMethod != null) {
sameSig = sameSig &&
Arrays.equals(candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes());
sameSig = sameSig && Arrays.equals(
candidateMethod.getGenericParameterTypes(), previousMethod.getGenericParameterTypes());
}
previousMethod = candidateMethod;
}
@@ -163,7 +162,8 @@ public final class BridgeMethodResolver {
}
}
// A non-array type: compare the type itself.
if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) {
if (!ClassUtils.resolvePrimitiveIfNecessary(candidateParameter).equals(
ClassUtils.resolvePrimitiveIfNecessary(genericParameter.toClass()))) {
return false;
}
}
@@ -226,8 +226,8 @@ public final class BridgeMethodResolver {
/**
* Compare the signatures of the bridge method and the method which it bridges. If
* the parameter and return types are the same, it is a 'visibility' bridge method
* introduced in Java 6 to fix https://bugs.openjdk.org/browse/JDK-6342411.
* See also https://stas-blogspot.blogspot.com/2010/03/java-bridge-methods-explained.html
* introduced in Java 6 to fix <a href="https://bugs.openjdk.org/browse/JDK-6342411">
* JDK-6342411</a>.
* @return whether signatures match as described
*/
public static boolean isVisibilityBridgeMethodPair(Method bridgeMethod, Method bridgedMethod) {
@@ -1049,16 +1049,16 @@ public abstract class AnnotationUtils {
return null;
}
try {
Method method = annotation.annotationType().getDeclaredMethod(attributeName);
return invokeAnnotationMethod(method, annotation);
}
catch (NoSuchMethodException ex) {
return null;
for (Method method : annotation.annotationType().getDeclaredMethods()) {
if (method.getName().equals(attributeName) && method.getParameterCount() == 0) {
return invokeAnnotationMethod(method, annotation);
}
}
}
catch (Throwable ex) {
handleValueRetrievalFailure(annotation, ex);
return null;
}
return null;
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -225,6 +225,8 @@ public class Indexer extends SpelNodeImpl {
cf.loadTarget(mv);
}
SpelNodeImpl index = this.children[0];
if (this.indexedType == IndexedType.ARRAY) {
int insn;
if ("D".equals(this.exitTypeDescriptor)) {
@@ -261,18 +263,14 @@ public class Indexer extends SpelNodeImpl {
//depthPlusOne(exitTypeDescriptor)+"Ljava/lang/Object;");
insn = AALOAD;
}
SpelNodeImpl index = this.children[0];
cf.enterCompilationScope();
index.generateCode(mv, cf);
cf.exitCompilationScope();
generateIndexCode(mv, cf, index, int.class);
mv.visitInsn(insn);
}
else if (this.indexedType == IndexedType.LIST) {
mv.visitTypeInsn(CHECKCAST, "java/util/List");
cf.enterCompilationScope();
this.children[0].generateCode(mv, cf);
cf.exitCompilationScope();
generateIndexCode(mv, cf, index, int.class);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true);
}
@@ -280,14 +278,12 @@ public class Indexer extends SpelNodeImpl {
mv.visitTypeInsn(CHECKCAST, "java/util/Map");
// Special case when the key is an unquoted string literal that will be parsed as
// a property/field reference
if ((this.children[0] instanceof PropertyOrFieldReference reference)) {
if (index instanceof PropertyOrFieldReference reference) {
String mapKeyName = reference.getName();
mv.visitLdcInsn(mapKeyName);
}
else {
cf.enterCompilationScope();
this.children[0].generateCode(mv, cf);
cf.exitCompilationScope();
generateIndexCode(mv, cf, index, Object.class);
}
mv.visitMethodInsn(
INVOKEINTERFACE, "java/util/Map", "get", "(Ljava/lang/Object;)Ljava/lang/Object;", true);
@@ -323,6 +319,11 @@ public class Indexer extends SpelNodeImpl {
cf.pushDescriptor(this.exitTypeDescriptor);
}
private void generateIndexCode(MethodVisitor mv, CodeFlow cf, SpelNodeImpl indexNode, Class<?> indexType) {
String indexDesc = CodeFlow.toDescriptor(indexType);
generateCodeForArgument(mv, cf, indexNode, indexDesc);
}
@Override
public String toStringAST() {
StringJoiner sj = new StringJoiner(",", "[", "]");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,6 +20,8 @@ import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -27,7 +29,10 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -132,6 +137,489 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
private SpelNodeImpl ast;
@Nested
class VariableReferenceTests {
@Test
void userDefinedVariable() {
EvaluationContext ctx = new StandardEvaluationContext();
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target");
assertThat(expression.getValue(ctx)).isEqualTo("abc");
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo("abc");
ctx.setVariable("target", "123");
assertThat(expression.getValue(ctx)).isEqualTo("123");
// Changing the variable type from String to Integer results in a
// ClassCastException in the compiled code.
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
ctx.setVariable("target", "abc");
expression = parser.parseExpression("#target.charAt(0)");
assertThat(expression.getValue(ctx)).isEqualTo('a');
assertCanCompile(expression);
assertThat(expression.getValue(ctx)).isEqualTo('a');
ctx.setVariable("target", "1");
assertThat(expression.getValue(ctx)).isEqualTo('1');
// Changing the variable type from String to Integer results in a
// ClassCastException in the compiled code.
ctx.setVariable("target", 42);
assertThatExceptionOfType(SpelEvaluationException.class)
.isThrownBy(() -> expression.getValue(ctx))
.withCauseInstanceOf(ClassCastException.class);
}
}
@Nested
class IndexingTests {
@Test
void indexIntoPrimitiveShortArray() {
short[] shorts = { (short) 33, (short) 44, (short) 55 };
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(shorts)).isEqualTo((short) 55);
assertCanCompile(expression);
assertThat(expression.getValue(shorts)).isEqualTo((short) 55);
assertThat(getAst().getExitDescriptor()).isEqualTo("S");
}
@Test
void indexIntoPrimitiveByteArray() {
byte[] bytes = { (byte) 2, (byte) 3, (byte) 4 };
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(bytes)).isEqualTo((byte) 4);
assertCanCompile(expression);
assertThat(expression.getValue(bytes)).isEqualTo((byte) 4);
assertThat(getAst().getExitDescriptor()).isEqualTo("B");
}
@Test
void indexIntoPrimitiveIntArray() {
int[] ints = { 8, 9, 10 };
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(ints)).isEqualTo(10);
assertCanCompile(expression);
assertThat(expression.getValue(ints)).isEqualTo(10);
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
}
@Test
void indexIntoPrimitiveLongArray() {
long[] longs = { 2L, 3L, 4L };
expression = parser.parseExpression("[0]");
assertThat(expression.getValue(longs)).isEqualTo(2L);
assertCanCompile(expression);
assertThat(expression.getValue(longs)).isEqualTo(2L);
assertThat(getAst().getExitDescriptor()).isEqualTo("J");
}
@Test
void indexIntoPrimitiveFloatArray() {
float[] floats = { 6.0f, 7.0f, 8.0f };
expression = parser.parseExpression("[0]");
assertThat(expression.getValue(floats)).isEqualTo(6.0f);
assertCanCompile(expression);
assertThat(expression.getValue(floats)).isEqualTo(6.0f);
assertThat(getAst().getExitDescriptor()).isEqualTo("F");
}
@Test
void indexIntoPrimitiveDoubleArray() {
double[] doubles = { 3.0d, 4.0d, 5.0d };
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(doubles)).isEqualTo(4.0d);
assertCanCompile(expression);
assertThat(expression.getValue(doubles)).isEqualTo(4.0d);
assertThat(getAst().getExitDescriptor()).isEqualTo("D");
}
@Test
void indexIntoPrimitiveCharArray() {
char[] chars = { 'a', 'b', 'c' };
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(chars)).isEqualTo('b');
assertCanCompile(expression);
assertThat(expression.getValue(chars)).isEqualTo('b');
assertThat(getAst().getExitDescriptor()).isEqualTo("C");
}
@Test
void indexIntoStringArray() {
String[] strings = { "a", "b", "c" };
expression = parser.parseExpression("[0]");
assertThat(expression.getValue(strings)).isEqualTo("a");
assertCanCompile(expression);
assertThat(expression.getValue(strings)).isEqualTo("a");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test
void indexIntoNumberArray() {
Number[] numbers = { 2, 8, 9 };
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(numbers)).isEqualTo(8);
assertCanCompile(expression);
assertThat(expression.getValue(numbers)).isEqualTo(8);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Number");
}
@Test
void indexInto2DPrimitiveIntArray() {
int[][] array = new int[][] {
{ 1, 2, 3 },
{ 4, 5, 6 }
};
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("4 5 6");
assertThat(getAst().getExitDescriptor()).isEqualTo("[I");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(array))).isEqualTo("6");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("6");
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
}
@Test
void indexInto2DStringArray() {
String[][] array = new String[][] {
{ "a", "b", "c" },
{ "d", "e", "f" }
};
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String");
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("[Ljava/lang/String");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test
@SuppressWarnings("unchecked")
void indexIntoArrayOfListOfString() {
List<String>[] array = new List[] {
List.of("a", "b", "c"),
List.of("d", "e", "f")
};
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/List");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
@SuppressWarnings("unchecked")
void indexIntoArrayOfMap() {
Map<String, String>[] array = new Map[] { Map.of("key", "value1") };
expression = parser.parseExpression("[0]");
assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map");
assertThat(stringify(expression.getValue(array))).isEqualTo("{key=value1}");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/util/Map");
expression = parser.parseExpression("[0]['key']");
assertThat(stringify(expression.getValue(array))).isEqualTo("value1");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(array))).isEqualTo("value1");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoListOfString() {
List<String> list = List.of("aaa", "bbb", "ccc");
expression = parser.parseExpression("[1]");
assertThat(expression.getValue(list)).isEqualTo("bbb");
assertCanCompile(expression);
assertThat(expression.getValue(list)).isEqualTo("bbb");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoListOfInteger() {
List<Integer> list = List.of(123, 456, 789);
expression = parser.parseExpression("[2]");
assertThat(expression.getValue(list)).isEqualTo(789);
assertCanCompile(expression);
assertThat(expression.getValue(list)).isEqualTo(789);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoListOfStringArray() {
List<String[]> list = List.of(
new String[] { "a", "b", "c" },
new String[] { "d", "e", "f" }
);
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("[1][0]");
assertThat(stringify(expression.getValue(list))).isEqualTo("d");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("d");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test
void indexIntoListOfIntegerArray() {
List<Integer[]> list = List.of(
new Integer[] { 1, 2, 3 },
new Integer[] { 4, 5, 6 }
);
expression = parser.parseExpression("[0]");
assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("1 2 3");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("[0][1]");
assertThat(expression.getValue(list)).isEqualTo(2);
assertCanCompile(expression);
assertThat(expression.getValue(list)).isEqualTo(2);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Integer");
}
@Test
void indexIntoListOfListOfString() {
List<List<String>> list = List.of(
List.of("a", "b", "c"),
List.of("d", "e", "f")
);
expression = parser.parseExpression("[1]");
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
assertThat(stringify(expression.getValue(list))).isEqualTo("d e f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("[1][2]");
assertThat(stringify(expression.getValue(list))).isEqualTo("f");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(list))).isEqualTo("f");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoMap() {
Map<String, Integer> map = Map.of("aaa", 111);
expression = parser.parseExpression("['aaa']");
assertThat(expression.getValue(map)).isEqualTo(111);
assertCanCompile(expression);
assertThat(expression.getValue(map)).isEqualTo(111);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoMapOfListOfString() {
Map<String, List<String>> map = Map.of("foo", List.of("a", "b", "c"));
expression = parser.parseExpression("['foo']");
assertThat(stringify(expression.getValue(map))).isEqualTo("a b c");
assertCanCompile(expression);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
assertThat(stringify(expression.getValue(map))).isEqualTo("a b c");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
expression = parser.parseExpression("['foo'][2]");
assertThat(stringify(expression.getValue(map))).isEqualTo("c");
assertCanCompile(expression);
assertThat(stringify(expression.getValue(map))).isEqualTo("c");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test
void indexIntoObject() {
TestClass6 tc = new TestClass6();
// field access
expression = parser.parseExpression("['orange']");
assertThat(expression.getValue(tc)).isEqualTo("value1");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value1");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
// field access
expression = parser.parseExpression("['peach']");
assertThat(expression.getValue(tc)).isEqualTo(34L);
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo(34L);
assertThat(getAst().getExitDescriptor()).isEqualTo("J");
// property access (getter)
expression = parser.parseExpression("['banana']");
assertThat(expression.getValue(tc)).isEqualTo("value3");
assertCanCompile(expression);
assertThat(expression.getValue(tc)).isEqualTo("value3");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/String");
}
@Test // gh-32694, gh-32908
void indexIntoArrayUsingIntegerWrapper() {
context.setVariable("array", new int[] {1, 2, 3, 4});
context.setVariable("index", 2);
expression = parser.parseExpression("#array[#index]");
assertThat(expression.getValue(context)).isEqualTo(3);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(3);
assertThat(getAst().getExitDescriptor()).isEqualTo("I");
}
@Test // gh-32694, gh-32908
void indexIntoListUsingIntegerWrapper() {
context.setVariable("list", List.of(1, 2, 3, 4));
context.setVariable("index", 2);
expression = parser.parseExpression("#list[#index]");
assertThat(expression.getValue(context)).isEqualTo(3);
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo(3);
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
@Test // gh-32903
void indexIntoMapUsingPrimitiveLiteral() {
Map<Object, String> map = Map.of(
false, "0", // BooleanLiteral
1, "ABC", // IntLiteral
2L, "XYZ", // LongLiteral
9.99F, "~10", // FloatLiteral
3.14159, "PI" // RealLiteral
);
context.setVariable("map", map);
// BooleanLiteral
expression = parser.parseExpression("#map[false]");
assertThat(expression.getValue(context)).isEqualTo("0");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("0");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// IntLiteral
expression = parser.parseExpression("#map[1]");
assertThat(expression.getValue(context)).isEqualTo("ABC");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("ABC");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// LongLiteral
expression = parser.parseExpression("#map[2L]");
assertThat(expression.getValue(context)).isEqualTo("XYZ");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("XYZ");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// FloatLiteral
expression = parser.parseExpression("#map[9.99F]");
assertThat(expression.getValue(context)).isEqualTo("~10");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("~10");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
// RealLiteral
expression = parser.parseExpression("#map[3.14159]");
assertThat(expression.getValue(context)).isEqualTo("PI");
assertCanCompile(expression);
assertThat(expression.getValue(context)).isEqualTo("PI");
assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object");
}
private String stringify(Object object) {
Stream<? extends Object> stream;
if (object instanceof Collection<?> collection) {
stream = collection.stream();
}
else if (object instanceof Object[] objects) {
stream = Arrays.stream(objects);
}
else if (object instanceof int[] ints) {
stream = Arrays.stream(ints).mapToObj(Integer::valueOf);
}
else {
return String.valueOf(object);
}
return stream.map(Object::toString).collect(Collectors.joining(" "));
}
}
@Test
void typeReference() {
expression = parse("T(String)");
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -154,7 +154,7 @@ final class PersistenceUnitReader {
/**
* Validate the given stream and return a valid DOM document for parsing.
*/
protected Document buildDocument(ErrorHandler handler, InputStream stream)
Document buildDocument(ErrorHandler handler, InputStream stream)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
@@ -168,9 +168,7 @@ final class PersistenceUnitReader {
/**
* Parse the validated document and add entries to the given unit info list.
*/
protected List<SpringPersistenceUnitInfo> parseDocument(
Resource resource, Document document, List<SpringPersistenceUnitInfo> infos) throws IOException {
void parseDocument(Resource resource, Document document, List<SpringPersistenceUnitInfo> infos) throws IOException {
Element persistence = document.getDocumentElement();
String version = persistence.getAttribute(PERSISTENCE_VERSION);
URL rootUrl = determinePersistenceUnitRootUrl(resource);
@@ -179,14 +177,12 @@ final class PersistenceUnitReader {
for (Element unit : units) {
infos.add(parsePersistenceUnitInfo(unit, version, rootUrl));
}
return infos;
}
/**
* Parse the unit info DOM element.
*/
protected SpringPersistenceUnitInfo parsePersistenceUnitInfo(
SpringPersistenceUnitInfo parsePersistenceUnitInfo(
Element persistenceUnit, String version, @Nullable URL rootUrl) throws IOException {
SpringPersistenceUnitInfo unitInfo = new SpringPersistenceUnitInfo();
@@ -253,7 +249,7 @@ final class PersistenceUnitReader {
/**
* Parse the {@code property} XML elements.
*/
protected void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
void parseProperties(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
Element propRoot = DomUtils.getChildElementByTagName(persistenceUnit, PROPERTIES);
if (propRoot == null) {
return;
@@ -269,7 +265,7 @@ final class PersistenceUnitReader {
/**
* Parse the {@code class} XML elements.
*/
protected void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
void parseManagedClasses(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
List<Element> classes = DomUtils.getChildElementsByTagName(persistenceUnit, MANAGED_CLASS_NAME);
for (Element element : classes) {
String value = DomUtils.getTextValue(element).trim();
@@ -282,7 +278,7 @@ final class PersistenceUnitReader {
/**
* Parse the {@code mapping-file} XML elements.
*/
protected void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
void parseMappingFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) {
List<Element> files = DomUtils.getChildElementsByTagName(persistenceUnit, MAPPING_FILE_NAME);
for (Element element : files) {
String value = DomUtils.getTextValue(element).trim();
@@ -295,7 +291,7 @@ final class PersistenceUnitReader {
/**
* Parse the {@code jar-file} XML elements.
*/
protected void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException {
void parseJarFiles(Element persistenceUnit, SpringPersistenceUnitInfo unitInfo) throws IOException {
List<Element> jars = DomUtils.getChildElementsByTagName(persistenceUnit, JAR_FILE_URL);
for (Element element : jars) {
String value = DomUtils.getTextValue(element).trim();
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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.
@@ -37,6 +37,8 @@ import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import javax.xml.XMLConstants;
import javax.xml.datatype.Duration;
@@ -192,7 +194,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
@Nullable
private ClassLoader beanClassLoader;
private final Object jaxbContextMonitor = new Object();
private final Lock jaxbContextLock = new ReentrantLock();
@Nullable
private volatile JAXBContext jaxbContext;
@@ -204,6 +206,12 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
private boolean processExternalEntities = false;
@Nullable
private volatile SAXParserFactory schemaParserFactory;
@Nullable
private volatile SAXParserFactory sourceParserFactory;
/**
* Set multiple JAXB context paths. The given array of context paths gets
@@ -426,6 +434,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
*/
public void setSupportDtd(boolean supportDtd) {
this.supportDtd = supportDtd;
this.sourceParserFactory = null;
}
/**
@@ -450,6 +459,7 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
if (processExternalEntities) {
this.supportDtd = true;
}
this.sourceParserFactory = null;
}
/**
@@ -497,7 +507,9 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
if (context != null) {
return context;
}
synchronized (this.jaxbContextMonitor) {
this.jaxbContextLock.lock();
try {
context = this.jaxbContext;
if (context == null) {
try {
@@ -521,6 +533,9 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
}
return context;
}
finally {
this.jaxbContextLock.unlock();
}
}
private JAXBContext createJaxbContextFromContextPath(String contextPath) throws JAXBException {
@@ -587,17 +602,24 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
Assert.notEmpty(resources, "No resources given");
Assert.hasLength(schemaLanguage, "No schema language provided");
Source[] schemaSources = new Source[resources.length];
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
SAXParserFactory saxParserFactory = this.schemaParserFactory;
if (saxParserFactory == null) {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
this.schemaParserFactory = saxParserFactory;
}
SAXParser saxParser = saxParserFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
for (int i = 0; i < resources.length; i++) {
Resource resource = resources[i];
Assert.isTrue(resource != null && resource.exists(), () -> "Resource does not exist: " + resource);
InputSource inputSource = SaxResourceUtils.createInputSource(resource);
schemaSources[i] = new SAXSource(xmlReader, inputSource);
}
SchemaFactory schemaFactory = SchemaFactory.newInstance(schemaLanguage);
if (this.schemaResourceResolver != null) {
schemaFactory.setResourceResolver(this.schemaResourceResolver);
@@ -886,11 +908,16 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi
try {
if (xmlReader == null) {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
String name = "http://xml.org/sax/features/external-general-entities";
saxParserFactory.setFeature(name, isProcessExternalEntities());
SAXParserFactory saxParserFactory = this.sourceParserFactory;
if (saxParserFactory == null) {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
saxParserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.sourceParserFactory = saxParserFactory;
}
SAXParser saxParser = saxParserFactory.newSAXParser();
xmlReader = saxParser.getXMLReader();
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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,9 +83,10 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
private boolean processExternalEntities = false;
@Nullable
private DocumentBuilderFactory documentBuilderFactory;
private volatile DocumentBuilderFactory documentBuilderFactory;
private final Object documentBuilderFactoryMonitor = new Object();
@Nullable
private volatile SAXParserFactory saxParserFactory;
/**
@@ -94,6 +95,8 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
*/
public void setSupportDtd(boolean supportDtd) {
this.supportDtd = supportDtd;
this.documentBuilderFactory = null;
this.saxParserFactory = null;
}
/**
@@ -118,6 +121,8 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
if (processExternalEntities) {
this.supportDtd = true;
}
this.documentBuilderFactory = null;
this.saxParserFactory = null;
}
/**
@@ -137,14 +142,13 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
*/
protected Document buildDocument() {
try {
DocumentBuilder documentBuilder;
synchronized (this.documentBuilderFactoryMonitor) {
if (this.documentBuilderFactory == null) {
this.documentBuilderFactory = createDocumentBuilderFactory();
}
documentBuilder = createDocumentBuilder(this.documentBuilderFactory);
DocumentBuilderFactory builderFactory = this.documentBuilderFactory;
if (builderFactory == null) {
builderFactory = createDocumentBuilderFactory();
this.documentBuilderFactory = builderFactory;
}
return documentBuilder.newDocument();
DocumentBuilder builder = createDocumentBuilder(builderFactory);
return builder.newDocument();
}
catch (ParserConfigurationException ex) {
throw new UnmarshallingFailureException("Could not create document placeholder: " + ex.getMessage(), ex);
@@ -179,11 +183,11 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
protected DocumentBuilder createDocumentBuilder(DocumentBuilderFactory factory)
throws ParserConfigurationException {
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
DocumentBuilder builder = factory.newDocumentBuilder();
if (!isProcessExternalEntities()) {
documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
builder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
}
return documentBuilder;
return builder;
}
/**
@@ -193,11 +197,17 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller {
* @throws ParserConfigurationException if thrown by JAXP methods
*/
protected XMLReader createXmlReader() throws SAXException, ParserConfigurationException {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
SAXParser saxParser = saxParserFactory.newSAXParser();
SAXParserFactory parserFactory = this.saxParserFactory;
if (parserFactory == null) {
parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
parserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
parserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.saxParserFactory = parserFactory;
}
SAXParser saxParser = parserFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
if (!isProcessExternalEntities()) {
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
@@ -100,9 +100,6 @@ public class MockHttpServletRequest implements HttpServletRequest {
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
private static final BufferedReader EMPTY_BUFFERED_READER =
new BufferedReader(new StringReader(""));
/**
* Date formats as specified in the HTTP RFC.
* @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a>
@@ -736,7 +733,7 @@ public class MockHttpServletRequest implements HttpServletRequest {
this.reader = new BufferedReader(sourceReader);
}
else {
this.reader = EMPTY_BUFFERED_READER;
this.reader = new BufferedReader(new StringReader(""));
}
return this.reader;
}
@@ -93,6 +93,17 @@ class MockHttpServletRequestTests {
secondRequest.getInputStream().close();
}
@Test // gh-32820
void readEmptyReaderWorksAcrossRequests() throws IOException {
MockHttpServletRequest firstRequest = new MockHttpServletRequest();
firstRequest.getReader().read(new char[256]);
firstRequest.getReader().close();
MockHttpServletRequest secondRequest = new MockHttpServletRequest();
secondRequest.getReader().read(new char[256]);
secondRequest.getReader().close();
}
@Test
void setContentAndGetReader() throws IOException {
byte[] bytes = "body".getBytes(Charset.defaultCharset());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2024 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.
@@ -61,6 +61,7 @@ import org.springframework.util.ClassUtils;
* @author Arjen Poutsma
* @author Sebastien Deleuze
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 3.0
* @see MarshallingHttpMessageConverter
*/
@@ -70,6 +71,9 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
private boolean processExternalEntities = false;
@Nullable
private volatile SAXParserFactory sourceParserFactory;
/**
* Indicate whether DTD parsing should be supported.
@@ -77,6 +81,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
*/
public void setSupportDtd(boolean supportDtd) {
this.supportDtd = supportDtd;
this.sourceParserFactory = null;
}
/**
@@ -97,6 +102,7 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
if (processExternalEntities) {
this.supportDtd = true;
}
this.sourceParserFactory = null;
}
/**
@@ -156,11 +162,16 @@ public class Jaxb2RootElementHttpMessageConverter extends AbstractJaxb2HttpMessa
if (source instanceof StreamSource streamSource) {
InputSource inputSource = new InputSource(streamSource.getInputStream());
try {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
String featureName = "http://xml.org/sax/features/external-general-entities";
saxParserFactory.setFeature(featureName, isProcessExternalEntities());
SAXParserFactory saxParserFactory = this.sourceParserFactory;
if (saxParserFactory == null) {
saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
saxParserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.sourceParserFactory = saxParserFactory;
}
SAXParser saxParser = saxParserFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
if (!isProcessExternalEntities()) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 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,6 +63,7 @@ import org.springframework.util.StreamUtils;
*
* @author Arjen Poutsma
* @author Rossen Stoyanchev
* @author Juergen Hoeller
* @since 3.0
* @param <T> the converted object type
*/
@@ -75,11 +76,7 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
(publicID, systemID, base, ns) -> InputStream.nullInputStream();
private static final Set<Class<?>> SUPPORTED_CLASSES = Set.of(
DOMSource.class,
SAXSource.class,
StAXSource.class,
StreamSource.class,
Source.class);
DOMSource.class, SAXSource.class, StAXSource.class, StreamSource.class, Source.class);
private final TransformerFactory transformerFactory = TransformerFactory.newInstance();
@@ -88,10 +85,19 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private boolean processExternalEntities = false;
@Nullable
private volatile DocumentBuilderFactory documentBuilderFactory;
@Nullable
private volatile SAXParserFactory saxParserFactory;
@Nullable
private volatile XMLInputFactory xmlInputFactory;
/**
* Sets the {@link #setSupportedMediaTypes(java.util.List) supportedMediaTypes}
* to {@code text/xml} and {@code application/xml}, and {@code application/*-xml}.
* to {@code text/xml} and {@code application/xml}, and {@code application/*+xml}.
*/
public SourceHttpMessageConverter() {
super(MediaType.APPLICATION_XML, MediaType.TEXT_XML, new MediaType("application", "*+xml"));
@@ -104,6 +110,9 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
*/
public void setSupportDtd(boolean supportDtd) {
this.supportDtd = supportDtd;
this.documentBuilderFactory = null;
this.saxParserFactory = null;
this.xmlInputFactory = null;
}
/**
@@ -124,6 +133,9 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
if (processExternalEntities) {
this.supportDtd = true;
}
this.documentBuilderFactory = null;
this.saxParserFactory = null;
this.xmlInputFactory = null;
}
/**
@@ -165,17 +177,21 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private DOMSource readDOMSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
try {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
documentBuilderFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
documentBuilderFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
if (!isProcessExternalEntities()) {
documentBuilder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
DocumentBuilderFactory builderFactory = this.documentBuilderFactory;
if (builderFactory == null) {
builderFactory = DocumentBuilderFactory.newInstance();
builderFactory.setNamespaceAware(true);
builderFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
builderFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.documentBuilderFactory = builderFactory;
}
Document document = documentBuilder.parse(body);
DocumentBuilder builder = builderFactory.newDocumentBuilder();
if (!isProcessExternalEntities()) {
builder.setEntityResolver(NO_OP_ENTITY_RESOLVER);
}
Document document = builder.parse(body);
return new DOMSource(document);
}
catch (NullPointerException ex) {
@@ -197,11 +213,17 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private SAXSource readSAXSource(InputStream body, HttpInputMessage inputMessage) throws IOException {
try {
SAXParserFactory saxParserFactory = SAXParserFactory.newInstance();
saxParserFactory.setNamespaceAware(true);
saxParserFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
saxParserFactory.setFeature("http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
SAXParser saxParser = saxParserFactory.newSAXParser();
SAXParserFactory parserFactory = this.saxParserFactory;
if (parserFactory == null) {
parserFactory = SAXParserFactory.newInstance();
parserFactory.setNamespaceAware(true);
parserFactory.setFeature(
"http://apache.org/xml/features/disallow-doctype-decl", !isSupportDtd());
parserFactory.setFeature(
"http://xml.org/sax/features/external-general-entities", isProcessExternalEntities());
this.saxParserFactory = parserFactory;
}
SAXParser saxParser = parserFactory.newSAXParser();
XMLReader xmlReader = saxParser.getXMLReader();
if (!isProcessExternalEntities()) {
xmlReader.setEntityResolver(NO_OP_ENTITY_RESOLVER);
@@ -217,11 +239,15 @@ public class SourceHttpMessageConverter<T extends Source> extends AbstractHttpMe
private Source readStAXSource(InputStream body, HttpInputMessage inputMessage) {
try {
XMLInputFactory inputFactory = XMLInputFactory.newInstance();
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd());
inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities());
if (!isProcessExternalEntities()) {
inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
XMLInputFactory inputFactory = this.xmlInputFactory;
if (inputFactory == null) {
inputFactory = XMLInputFactory.newInstance();
inputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, isSupportDtd());
inputFactory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, isProcessExternalEntities());
if (!isProcessExternalEntities()) {
inputFactory.setXMLResolver(NO_OP_XML_RESOLVER);
}
this.xmlInputFactory = inputFactory;
}
XMLStreamReader streamReader = inputFactory.createXMLStreamReader(body);
return new StAXSource(streamReader);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,9 +21,12 @@ import java.util.Optional;
import io.micrometer.observation.Observation;
import io.micrometer.observation.ObservationRegistry;
import jakarta.servlet.AsyncEvent;
import jakarta.servlet.AsyncListener;
import jakarta.servlet.FilterChain;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@@ -94,11 +97,6 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
return Optional.ofNullable((ServerRequestObservationContext) request.getAttribute(CURRENT_OBSERVATION_CONTEXT_ATTRIBUTE));
}
@Override
protected boolean shouldNotFilterAsyncDispatch() {
return false;
}
@Override
@SuppressWarnings("try")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
@@ -114,8 +112,12 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
throw ex;
}
finally {
// Only stop Observation if async processing is done or has never been started.
if (!request.isAsyncStarted()) {
// If async is started, register a listener for completion notification.
if (request.isAsyncStarted()) {
request.getAsyncContext().addListener(new ObservationAsyncListener(observation));
}
// Stop Observation right now if async processing has not been started.
else {
Throwable error = fetchException(request);
if (error != null) {
observation.error(error);
@@ -139,13 +141,44 @@ public class ServerHttpObservationFilter extends OncePerRequestFilter {
return observation;
}
private Throwable unwrapServletException(Throwable ex) {
@Nullable
static Throwable unwrapServletException(Throwable ex) {
return (ex instanceof ServletException) ? ex.getCause() : ex;
}
@Nullable
private Throwable fetchException(HttpServletRequest request) {
static Throwable fetchException(ServletRequest request) {
return (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
}
private static class ObservationAsyncListener implements AsyncListener {
private final Observation currentObservation;
public ObservationAsyncListener(Observation currentObservation) {
this.currentObservation = currentObservation;
}
@Override
public void onStartAsync(AsyncEvent event) {
}
@Override
public void onTimeout(AsyncEvent event) {
this.currentObservation.stop();
}
@Override
public void onComplete(AsyncEvent event) {
this.currentObservation.stop();
}
@Override
public void onError(AsyncEvent event) {
this.currentObservation.error(unwrapServletException(event.getThrowable()));
this.currentObservation.stop();
}
}
}
@@ -50,6 +50,11 @@ class ServerHttpObservationFilterTests {
private final MockHttpServletResponse response = new MockHttpServletResponse();
@Test
void filterShouldNotProcessAsyncDispatch() {
assertThat(this.filter.shouldNotFilterAsyncDispatch()).isTrue();
}
@Test
void filterShouldFillObservationContext() throws Exception {
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
@@ -60,7 +65,7 @@ class ServerHttpObservationFilterTests {
assertThat(context.getCarrier()).isEqualTo(this.request);
assertThat(context.getResponse()).isEqualTo(this.response);
assertThat(context.getPathPattern()).isNull();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS");
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
}
@Test
@@ -111,6 +116,16 @@ class ServerHttpObservationFilterTests {
.hasLowCardinalityKeyValue("status", "500");
}
@Test
void shouldCloseObservationAfterAsyncCompletion() throws Exception {
this.request.setAsyncSupported(true);
this.request.startAsync();
this.filter.doFilter(this.request, this.response, this.mockFilterChain);
this.request.getAsyncContext().complete();
assertThatHttpObservation().hasLowCardinalityKeyValue("outcome", "SUCCESS").hasBeenStopped();
}
private TestObservationRegistryAssert.TestObservationRegistryAssertReturningObservationContextAssert assertThatHttpObservation() {
return TestObservationRegistryAssert.assertThat(this.observationRegistry)
.hasObservationWithNameEqualTo("http.server.requests").that();