Compare commits

...

38 Commits

Author SHA1 Message Date
Spring Builds 23bb745ea4 Release v5.3.13 2021-11-11 07:31:25 +00:00
Arjen Poutsma 9197f15a30 Use ByteArrayDecoder in DefaultClientResponse::createException
This commit changes DefaultClientResponse::createException to use
the ByteArrayDecoder, instead of converting to DataBuffers and
turning these into a byte array.

Closes gh-27666
2021-11-10 16:24:49 +01:00
Juergen Hoeller 15a6373fed Avoid nullability warnings 2021-11-10 14:22:06 +01:00
Juergen Hoeller 2a268701c4 Introduce optimizeLocations flag for resource location filtering on startup
This flag is off by default since it requires jar files with directory entries.

Closes gh-27624
2021-11-10 14:21:48 +01:00
Juergen Hoeller 11a0df3fd1 Upgrade to Tomcat 9.0.54, Netty 4.1.70, HtmlUnit 2.54, XMLUnit 2.8.3 2021-11-09 22:36:11 +01:00
Juergen Hoeller b167e1a93f Remove unnecessary final declarations (for consistency) 2021-11-09 22:35:24 +01:00
Juergen Hoeller 8d735e6e60 Note on PersistenceExceptionTranslator sorting and non-singleton retrieval
Closes gh-26412
2021-11-09 22:30:44 +01:00
Juergen Hoeller 9bc09631d6 Recommend ObjectProvider as alternative to @Lazy for optional dependencies
Closes gh-27649
2021-11-09 22:29:56 +01:00
Stephane Nicoll 39be572a7a Upgrade to Reactor 2020.0.13
Closes gh-27636
2021-11-09 19:38:10 +01:00
Abdullah Khan 6ac780d595 Fix grammar in webflux-webclient.adoc
Closes gh-27657
2021-11-09 14:22:50 +01:00
Rossen Stoyanchev c6ce65ef56 Polishing contribution
Closes gh-27623
2021-11-09 09:47:02 +00:00
happyWilliam0 b5743966d6 Improve efficiency of UrlPathHelper#getSanitizedPath
See gh-27623
2021-11-09 09:47:02 +00:00
Rossen Stoyanchev c5de5c9939 Update Javadoc of LogFormatUtils
Closes gh-27632
2021-11-08 21:21:54 +00:00
Sam Brannen ff1485fd8d Fix log message formatting in CandidateComponentsIndexLoader 2021-11-05 10:31:10 +01:00
Stephane Nicoll 1b4438caac Start building against Reactor 2020.0.13 snapshots
See gh-27636
2021-11-04 14:47:37 +01:00
Arjen Poutsma 0c7e000250 Refactor Contents to DefaultParts
This commit moves the Contents abstraction into DefaultParts

See gh-27613
2021-11-03 15:02:12 +01:00
Arjen Poutsma 694db2273f Add Part::delete method
This commit introduces the Part::delete method, that deletes its
underlying storage.

Closes gh-27612
2021-11-02 16:15:44 +01:00
Stephane Nicoll 47d3819eab Merge pull request #27604 from Xjzon
* pr/27604:
  Polish "Add support for custom expression parsing"
  Add support for custom expression parsing

Closes gh-27604
2021-10-28 14:21:29 +02:00
Stephane Nicoll f295c8b2fb Polish "Add support for custom expression parsing"
See gh-27604
2021-10-28 14:20:15 +02:00
Xjzon 40d84c297b Add support for custom expression parsing
See gh-27604
2021-10-28 13:57:15 +02:00
Arjen Poutsma 0416168d0e Fix bug in max header calculation in DefaultPartHttpMessageReader
This commit fixes a bug in the DefaultPartHttpMessageReader, in the
check for exceeding the maximum header size. Before this commit, the
entire buffer size was considered, thus triggering an exception even
though the max header limit was not exceeded. After this commit, we only
consider the size up until the end-of-header mark (CRLFCRLF).

Furthermore, this commit increases the default maximum header size to
10k, the same default as Commons File upload.

Closes gh-27612
2021-10-27 16:28:32 +02:00
Sam Brannen c4c3d59d07 Remove unused code in FormattingConversionServiceTests 2021-10-25 17:02:58 +02:00
Сергей Цыпанов 0268e76298 Improve mapping function in ExtendedEntityManagerCreator.createProxy()
Closes gh-27456
2021-10-25 16:30:26 +02:00
Kenzo Spaulding 4ec8ae42f4 Add test cases to SpEL's MapAccessorTests
Closes gh-26981
2021-10-23 17:45:37 +02:00
Sam Brannen 995aaa6159 Polishing 2021-10-23 16:08:00 +02:00
Sam Brannen 2c89ff934d Update copyright date
See gh-27560
2021-10-23 15:59:46 +02:00
Daniil Pozdeev c58853f5e5 Use LocalDataSourceJobStore only if one is not set via Quartz properties
Prior to this commit, Spring's SchedulerFactoryBean always set the
"org.quartz.jobStore.class" property to LocalDataSourceJobStore even if
the user had already specified a custom JobStore implementation via the
Quartz properties file or Properties object, thereby effectively
ignoring the user configuration.

This commit addresses this by configuring Quartz to use Spring's
LocalDataSourceJobStore only if a JobStore has not already been
specified via user configuration.

Closes gh-27560
2021-10-23 15:22:42 +02:00
xxpain 66826ac960 Fix typo in Javadoc in HeaderAssertions
Closes gh-27589
2021-10-22 15:28:27 +02:00
Sam Brannen a603779f33 Return previous value in UndertowHeadersAdapter's remove() method
Prior to this commit, UndertowHeadersAdapter's remove() method violated
the java.util.Map contract by always returning null.

This commit fixes this by returning the previous list stored under the
specified key, and otherwise returning null if no previous value was
present.

Closes gh-27592
2021-10-22 14:57:12 +02:00
Сергей Цыпанов e5475d698a Iterate over Map's entrySet() instead of keySet() in PropertyEditorRegistrySupport
Closes gh-27591
2021-10-22 14:15:33 +02:00
Sam Brannen b728b4640b Add explicit tests for SpEL functions for fix for gh-27582 2021-10-22 13:46:11 +02:00
Sam Brannen bc657eb4d5 Fix SpEL vararg method invocation for strings containing commas
Prior to this commit, if a SpEL expression invoked a method or
registered function that declares a String varargs argument, there were
sometimes issues with converting the input arguments into the varargs
array argument. Specifically, if the expression supplied a single
String argument containing a comma for the varargs (such as "a,b"),
SpEL's ReflectionHelper.convertArguments() method incorrectly converted
that single String to an array via the ConversionService, which
indirectly converted that String using the StringToArrayConverter,
which converts a comma-delimited String to an array. Thus, "a,b"
effectively got converted to a two-dimensional array ["a", "b"] instead
of simply ["a,b"].

This commit fixes this bug by avoiding use of the TypeConverter and
ConversionService for single arguments supplied as varargs when the
single argument's type matches the varargs array component type.

Closes gh-27582
2021-10-22 13:13:13 +02:00
Sam Brannen 9b967775ed Polish Spel's ReflectionHelper.setupArgumentsForVarargsInvocation() 2021-10-22 11:08:45 +02:00
Sam Brannen 9af11ad5ce Fix Javadoc formatting issues 2021-10-22 11:08:45 +02:00
Sam Brannen 7a54ff2d21 Extract public TypeFilterUtils from ComponentScanAnnotationParser
Prior to this commit, third parties using @ComponentScan's @Filter
annotation had to implement their own parsing for @Filter
AnnotationAttributes as well as instantiation of the corresponding
TypeFilters. In such cases the various *Aware callbacks
(BeanFactoryAware, EnvironmentAware, etc.) should also be supported.

This commit therefore extracts a new public TypeFilterUtils class from
ComponentScanAnnotationParser so that third parties can benefit from
consistent TypeFilter creation from @ComponentScan @Filter annotations.

Closes gh-27553
2021-10-21 14:15:38 +02:00
Sam Brannen ec3f857bda Polish contribution
See gh-27586
2021-10-21 12:40:10 +02:00
no-brand b3eb1a2ad7 Improve example in Javadoc for HttpEntity
Closes gh-27586
2021-10-21 12:35:59 +02:00
Spring Builds bce850aa12 Next development version (v5.3.13-SNAPSHOT) 2021-10-21 05:57:00 +00:00
55 changed files with 874 additions and 364 deletions
+7 -7
View File
@@ -28,8 +28,8 @@ configure(allprojects) { project ->
dependencyManagement {
imports {
mavenBom "com.fasterxml.jackson:jackson-bom:2.12.5"
mavenBom "io.netty:netty-bom:4.1.69.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.12"
mavenBom "io.netty:netty-bom:4.1.70.Final"
mavenBom "io.projectreactor:reactor-bom:2020.0.13"
mavenBom "io.r2dbc:r2dbc-bom:Arabba-SR10"
mavenBom "io.rsocket:rsocket-bom:1.1.1"
mavenBom "org.eclipse.jetty:jetty-bom:9.4.44.v20210927"
@@ -128,14 +128,14 @@ configure(allprojects) { project ->
dependency "org.webjars:webjars-locator-core:0.48"
dependency "org.webjars:underscorejs:1.8.3"
dependencySet(group: 'org.apache.tomcat', version: '9.0.53') {
dependencySet(group: 'org.apache.tomcat', version: '9.0.54') {
entry 'tomcat-util'
entry('tomcat-websocket') {
exclude group: "org.apache.tomcat", name: "tomcat-websocket-api"
exclude group: "org.apache.tomcat", name: "tomcat-servlet-api"
}
}
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.53') {
dependencySet(group: 'org.apache.tomcat.embed', version: '9.0.54') {
entry 'tomcat-embed-core'
entry 'tomcat-embed-websocket'
}
@@ -192,7 +192,7 @@ configure(allprojects) { project ->
dependency "org.hamcrest:hamcrest:2.1"
dependency "org.awaitility:awaitility:3.1.6"
dependency "org.assertj:assertj-core:3.21.0"
dependencySet(group: 'org.xmlunit', version: '2.8.2') {
dependencySet(group: 'org.xmlunit', version: '2.8.3') {
entry 'xmlunit-assertj'
entry('xmlunit-matchers') {
exclude group: "org.hamcrest", name: "hamcrest-core"
@@ -206,10 +206,10 @@ configure(allprojects) { project ->
}
dependency "io.mockk:mockk:1.12.0"
dependency("net.sourceforge.htmlunit:htmlunit:2.53.0") {
dependency("net.sourceforge.htmlunit:htmlunit:2.54.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:htmlunit-driver:2.53.0") {
dependency("org.seleniumhq.selenium:htmlunit-driver:2.54.0") {
exclude group: "commons-logging", name: "commons-logging"
}
dependency("org.seleniumhq.selenium:selenium-java:3.141.59") {
+1 -1
View File
@@ -1,4 +1,4 @@
version=5.3.12-SNAPSHOT
version=5.3.13
org.gradle.jvmargs=-Xmx1536M
org.gradle.caching=true
org.gradle.parallel=true
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -422,10 +422,13 @@ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry {
}
if (editor == null) {
// Find editor for superclass or interface.
for (Iterator<Class<?>> it = this.customEditors.keySet().iterator(); it.hasNext() && editor == null;) {
Class<?> key = it.next();
for (Map.Entry<Class<?>, PropertyEditor> entry : this.customEditors.entrySet()) {
if (editor != null) {
break;
}
Class<?> key = entry.getKey();
if (key.isAssignableFrom(requiredType)) {
editor = this.customEditors.get(key);
editor = entry.getValue();
// Cache editor for search type, to avoid the overhead
// of repeated assignable-from checks.
if (this.customEditorCache == null) {
@@ -459,7 +459,7 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA
return metadata;
}
private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
private InjectionMetadata buildAutowiringMetadata(Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -570,7 +570,7 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe
CollectionUtils.mergePropertiesIntoMap(this.quartzProperties, mergedProps);
if (this.dataSource != null) {
mergedProps.setProperty(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
mergedProps.putIfAbsent(StdSchedulerFactory.PROP_JOB_STORE_CLASS, LocalDataSourceJobStore.class.getName());
}
// Determine scheduler name across local settings and Quartz properties...
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@ package org.springframework.scheduling.quartz;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import javax.sql.DataSource;
@@ -30,6 +31,7 @@ import org.quartz.SchedulerContext;
import org.quartz.SchedulerFactory;
import org.quartz.impl.JobDetailImpl;
import org.quartz.impl.SchedulerRepository;
import org.quartz.impl.jdbcjobstore.JobStoreTX;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
@@ -40,6 +42,8 @@ import org.springframework.context.support.StaticApplicationContext;
import org.springframework.core.task.TaskExecutor;
import org.springframework.core.testfixture.EnabledForTestGroups;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@@ -57,10 +61,10 @@ import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING;
* @author Sam Brannen
* @since 20.02.2004
*/
public class QuartzSupportTests {
class QuartzSupportTests {
@Test
public void schedulerFactoryBeanWithApplicationContext() throws Exception {
void schedulerFactoryBeanWithApplicationContext() throws Exception {
TestBean tb = new TestBean("tb", 99);
StaticApplicationContext ac = new StaticApplicationContext();
@@ -97,7 +101,7 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerWithTaskExecutor() throws Exception {
void schedulerWithTaskExecutor() throws Exception {
CountingTaskExecutor taskExecutor = new CountingTaskExecutor();
DummyJob.count = 0;
@@ -130,7 +134,7 @@ public class QuartzSupportTests {
@Test
@SuppressWarnings({ "unchecked", "rawtypes" })
public void jobDetailWithRunnableInsteadOfJob() {
void jobDetailWithRunnableInsteadOfJob() {
JobDetailImpl jobDetail = new JobDetailImpl();
assertThatIllegalArgumentException().isThrownBy(() ->
jobDetail.setJobClass((Class) DummyRunnable.class));
@@ -138,7 +142,7 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerWithQuartzJobBean() throws Exception {
void schedulerWithQuartzJobBean() throws Exception {
DummyJob.param = 0;
DummyJob.count = 0;
@@ -171,7 +175,7 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerWithSpringBeanJobFactory() throws Exception {
void schedulerWithSpringBeanJobFactory() throws Exception {
DummyJob.param = 0;
DummyJob.count = 0;
@@ -206,7 +210,7 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception {
void schedulerWithSpringBeanJobFactoryAndParamMismatchNotIgnored() throws Exception {
DummyJob.param = 0;
DummyJob.count = 0;
@@ -242,7 +246,7 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception {
void schedulerWithSpringBeanJobFactoryAndQuartzJobBean() throws Exception {
DummyJobBean.param = 0;
DummyJobBean.count = 0;
@@ -276,7 +280,7 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception {
void schedulerWithSpringBeanJobFactoryAndJobSchedulingData() throws Exception {
DummyJob.param = 0;
DummyJob.count = 0;
@@ -294,7 +298,7 @@ public class QuartzSupportTests {
}
@Test // SPR-772
public void multipleSchedulers() throws Exception {
void multipleSchedulers() throws Exception {
try (ClassPathXmlApplicationContext ctx = context("multipleSchedulers.xml")) {
Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1");
Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2");
@@ -305,7 +309,7 @@ public class QuartzSupportTests {
}
@Test // SPR-16884
public void multipleSchedulersWithQuartzProperties() throws Exception {
void multipleSchedulersWithQuartzProperties() throws Exception {
try (ClassPathXmlApplicationContext ctx = context("multipleSchedulersWithQuartzProperties.xml")) {
Scheduler scheduler1 = (Scheduler) ctx.getBean("scheduler1");
Scheduler scheduler2 = (Scheduler) ctx.getBean("scheduler2");
@@ -317,12 +321,13 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception {
Thread.sleep(3000);
void twoAnonymousMethodInvokingJobDetailFactoryBeans() throws Exception {
try (ClassPathXmlApplicationContext ctx = context("multipleAnonymousMethodInvokingJobDetailFB.xml")) {
QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService");
QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService");
Thread.sleep(400);
assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0);
assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2);
assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2);
@@ -332,12 +337,13 @@ public class QuartzSupportTests {
@Test
@EnabledForTestGroups(LONG_RUNNING)
public void schedulerAccessorBean() throws Exception {
Thread.sleep(3000);
void schedulerAccessorBean() throws Exception {
try (ClassPathXmlApplicationContext ctx = context("schedulerAccessorBean.xml")) {
QuartzTestBean exportService = (QuartzTestBean) ctx.getBean("exportService");
QuartzTestBean importService = (QuartzTestBean) ctx.getBean("importService");
Thread.sleep(400);
assertThat(exportService.getImportCount()).as("doImport called exportService").isEqualTo(0);
assertThat(exportService.getExportCount()).as("doExport not called on exportService").isEqualTo(2);
assertThat(importService.getImportCount()).as("doImport not called on importService").isEqualTo(2);
@@ -347,7 +353,7 @@ public class QuartzSupportTests {
@Test
@SuppressWarnings("resource")
public void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception {
void schedulerAutoStartsOnContextRefreshedEventByDefault() throws Exception {
StaticApplicationContext context = new StaticApplicationContext();
context.registerBeanDefinition("scheduler", new RootBeanDefinition(SchedulerFactoryBean.class));
Scheduler bean = context.getBean("scheduler", Scheduler.class);
@@ -358,7 +364,7 @@ public class QuartzSupportTests {
@Test
@SuppressWarnings("resource")
public void schedulerAutoStartupFalse() throws Exception {
void schedulerAutoStartupFalse() throws Exception {
StaticApplicationContext context = new StaticApplicationContext();
BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class)
.addPropertyValue("autoStartup", false).getBeanDefinition();
@@ -370,7 +376,7 @@ public class QuartzSupportTests {
}
@Test
public void schedulerRepositoryExposure() throws Exception {
void schedulerRepositoryExposure() throws Exception {
try (ClassPathXmlApplicationContext ctx = context("schedulerRepositoryExposure.xml")) {
assertThat(ctx.getBean("scheduler")).isSameAs(SchedulerRepository.getInstance().lookup("myScheduler"));
}
@@ -381,7 +387,7 @@ public class QuartzSupportTests {
* TODO: Against Quartz 2.2, this test's job doesn't actually execute anymore...
*/
@Test
public void schedulerWithHsqlDataSource() throws Exception {
void schedulerWithHsqlDataSource() throws Exception {
DummyJob.param = 0;
DummyJob.count = 0;
@@ -396,12 +402,36 @@ public class QuartzSupportTests {
}
}
@Test
@SuppressWarnings("resource")
void schedulerFactoryBeanWithCustomJobStore() throws Exception {
StaticApplicationContext context = new StaticApplicationContext();
String dbName = "mydb";
EmbeddedDatabase database = new EmbeddedDatabaseBuilder().setName(dbName).build();
Properties properties = new Properties();
properties.setProperty("org.quartz.jobStore.class", JobStoreTX.class.getName());
properties.setProperty("org.quartz.jobStore.dataSource", dbName);
BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(SchedulerFactoryBean.class)
.addPropertyValue("autoStartup", false)
.addPropertyValue("dataSource", database)
.addPropertyValue("quartzProperties", properties)
.getBeanDefinition();
context.registerBeanDefinition("scheduler", beanDefinition);
Scheduler scheduler = context.getBean(Scheduler.class);
assertThat(scheduler.getMetaData().getJobStoreClass()).isEqualTo(JobStoreTX.class);
}
private ClassPathXmlApplicationContext context(String path) {
return new ClassPathXmlApplicationContext(path, getClass());
}
public static class CountingTaskExecutor implements TaskExecutor {
private static class CountingTaskExecutor implements TaskExecutor {
private int count;
@@ -413,12 +443,14 @@ public class QuartzSupportTests {
}
public static class DummyJob implements Job {
private static class DummyJob implements Job {
private static int param;
private static int count;
@SuppressWarnings("unused")
// Must be public
public void setParam(int value) {
if (param > 0) {
throw new IllegalStateException("Param already set");
@@ -433,12 +465,13 @@ public class QuartzSupportTests {
}
public static class DummyJobBean extends QuartzJobBean {
private static class DummyJobBean extends QuartzJobBean {
private static int param;
private static int count;
@SuppressWarnings("unused")
public void setParam(int value) {
if (param > 0) {
throw new IllegalStateException("Param already set");
@@ -453,7 +486,7 @@ public class QuartzSupportTests {
}
public static class DummyRunnable implements Runnable {
private static class DummyRunnable implements Runnable {
@Override
public void run() {
@@ -19,7 +19,7 @@
<property name="targetMethod" value="doExport"/>
</bean>
</property>
<property name="repeatInterval" value="1000"/>
<property name="repeatInterval" value="200"/>
<property name="repeatCount" value="1"/>
</bean>
@@ -30,7 +30,7 @@
<property name="targetMethod" value="doImport"/>
</bean>
</property>
<property name="repeatInterval" value="1000"/>
<property name="repeatInterval" value="200"/>
<property name="repeatCount" value="1"/>
</bean>
@@ -21,7 +21,7 @@
<property name="targetMethod" value="doExport"/>
</bean>
</property>
<property name="repeatInterval" value="1000"/>
<property name="repeatInterval" value="200"/>
<property name="repeatCount" value="1"/>
</bean>
@@ -32,7 +32,7 @@
<property name="targetMethod" value="doImport"/>
</bean>
</property>
<property name="repeatInterval" value="1000"/>
<property name="repeatInterval" value="200"/>
<property name="repeatCount" value="1"/>
</bean>
@@ -343,7 +343,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
}
private InjectionMetadata findResourceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
private InjectionMetadata findResourceMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
// Fall back to class name as cache key, for backwards compatibility with custom callers.
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// Quick check on the concurrent map first, with minimal locking.
@@ -363,7 +363,7 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean
return metadata;
}
private InjectionMetadata buildResourceMetadata(final Class<?> clazz) {
private InjectionMetadata buildResourceMetadata(Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,13 +16,10 @@
package org.springframework.context.annotation;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
@@ -33,12 +30,7 @@ import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.filter.AbstractTypeHierarchyTraversingFilter;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AspectJTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
@@ -73,7 +65,7 @@ class ComponentScanAnnotationParser {
}
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
@@ -93,13 +85,17 @@ class ComponentScanAnnotationParser {
scanner.setResourcePattern(componentScan.getString("resourcePattern"));
for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
for (AnnotationAttributes includeFilterAttributes : componentScan.getAnnotationArray("includeFilters")) {
List<TypeFilter> typeFilters = TypeFilterUtils.createTypeFiltersFor(includeFilterAttributes, this.environment,
this.resourceLoader, this.registry);
for (TypeFilter typeFilter : typeFilters) {
scanner.addIncludeFilter(typeFilter);
}
}
for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
for (TypeFilter typeFilter : typeFiltersFor(filter)) {
for (AnnotationAttributes excludeFilterAttributes : componentScan.getAnnotationArray("excludeFilters")) {
List<TypeFilter> typeFilters = TypeFilterUtils.createTypeFiltersFor(excludeFilterAttributes, this.environment,
this.resourceLoader, this.registry);
for (TypeFilter typeFilter : typeFilters) {
scanner.addExcludeFilter(typeFilter);
}
}
@@ -132,49 +128,4 @@ class ComponentScanAnnotationParser {
return scanner.doScan(StringUtils.toStringArray(basePackages));
}
private List<TypeFilter> typeFiltersFor(AnnotationAttributes filterAttributes) {
List<TypeFilter> typeFilters = new ArrayList<>();
FilterType filterType = filterAttributes.getEnum("type");
for (Class<?> filterClass : filterAttributes.getClassArray("classes")) {
switch (filterType) {
case ANNOTATION:
Assert.isAssignable(Annotation.class, filterClass,
"@ComponentScan ANNOTATION type filter requires an annotation type");
@SuppressWarnings("unchecked")
Class<Annotation> annotationType = (Class<Annotation>) filterClass;
typeFilters.add(new AnnotationTypeFilter(annotationType));
break;
case ASSIGNABLE_TYPE:
typeFilters.add(new AssignableTypeFilter(filterClass));
break;
case CUSTOM:
Assert.isAssignable(TypeFilter.class, filterClass,
"@ComponentScan CUSTOM type filter requires a TypeFilter implementation");
TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class,
this.environment, this.resourceLoader, this.registry);
typeFilters.add(filter);
break;
default:
throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType);
}
}
for (String expression : filterAttributes.getStringArray("pattern")) {
switch (filterType) {
case ASPECTJ:
typeFilters.add(new AspectJTypeFilter(expression, this.resourceLoader.getClassLoader()));
break;
case REGEX:
typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression)));
break;
default:
throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType);
}
}
return typeFilters;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -453,8 +453,8 @@ class ConfigurationClassEnhancer {
* instance directly. If a FactoryBean instance is fetched through the container via &-dereferencing,
* it will not be proxied. This too is aligned with the way XML configuration works.
*/
private Object enhanceFactoryBean(final Object factoryBean, Class<?> exposedType,
final ConfigurableBeanFactory beanFactory, final String beanName) {
private Object enhanceFactoryBean(Object factoryBean, Class<?> exposedType,
ConfigurableBeanFactory beanFactory, String beanName) {
try {
Class<?> clazz = factoryBean.getClass();
@@ -489,8 +489,8 @@ class ConfigurationClassEnhancer {
return createCglibProxyForFactoryBean(factoryBean, beanFactory, beanName);
}
private Object createInterfaceProxyForFactoryBean(final Object factoryBean, Class<?> interfaceType,
final ConfigurableBeanFactory beanFactory, final String beanName) {
private Object createInterfaceProxyForFactoryBean(Object factoryBean, Class<?> interfaceType,
ConfigurableBeanFactory beanFactory, String beanName) {
return Proxy.newProxyInstance(
factoryBean.getClass().getClassLoader(), new Class<?>[] {interfaceType},
@@ -502,8 +502,8 @@ class ConfigurationClassEnhancer {
});
}
private Object createCglibProxyForFactoryBean(final Object factoryBean,
final ConfigurableBeanFactory beanFactory, final String beanName) {
private Object createCglibProxyForFactoryBean(Object factoryBean,
ConfigurableBeanFactory beanFactory, String beanName) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(factoryBean.getClass());
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2013 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -47,6 +47,11 @@ import java.lang.annotation.Target;
* or {@link javax.inject.Inject}: In that context, it leads to the creation of a
* lazy-resolution proxy for all affected dependencies, as an alternative to using
* {@link org.springframework.beans.factory.ObjectFactory} or {@link javax.inject.Provider}.
* Please note that such a lazy-resolution proxy will always be injected; if the target
* dependency does not exist, you will only be able to find out through an exception on
* invocation. As a consequence, such an injection point results in unintuitive behavior
* for optional dependencies. For a programmatic equivalent, allowing for lazy references
* with more sophistication, consider {@link org.springframework.beans.factory.ObjectProvider}.
*
* @author Chris Beams
* @author Juergen Hoeller
@@ -0,0 +1,119 @@
/*
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.context.annotation;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.type.filter.AnnotationTypeFilter;
import org.springframework.core.type.filter.AspectJTypeFilter;
import org.springframework.core.type.filter.AssignableTypeFilter;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.util.Assert;
/**
* Collection of utilities for working with {@link ComponentScan @ComponentScan}
* {@linkplain ComponentScan.Filter type filters}.
*
* @author Chris Beams
* @author Juergen Hoeller
* @author Sam Brannen
* @since 5.3.13
* @see ComponentScan.Filter
* @see org.springframework.core.type.filter.TypeFilter
*/
public abstract class TypeFilterUtils {
/**
* Create {@linkplain TypeFilter type filters} from the supplied
* {@link AnnotationAttributes}, such as those sourced from
* {@link ComponentScan#includeFilters()} or {@link ComponentScan#excludeFilters()}.
* <p>Each {@link TypeFilter} will be instantiated using an appropriate
* constructor, with {@code BeanClassLoaderAware}, {@code BeanFactoryAware},
* {@code EnvironmentAware}, and {@code ResourceLoaderAware} contracts
* invoked if they are implemented by the type filter.
* @param filterAttributes {@code AnnotationAttributes} for a
* {@link ComponentScan.Filter @Filter} declaration
* @param environment the {@code Environment} to make available to filters
* @param resourceLoader the {@code ResourceLoader} to make available to filters
* @param registry the {@code BeanDefinitionRegistry} to make available to filters
* as a {@link org.springframework.beans.factory.BeanFactory} if applicable
* @return a list of instantiated and configured type filters
* @see TypeFilter
* @see AnnotationTypeFilter
* @see AssignableTypeFilter
* @see AspectJTypeFilter
* @see RegexPatternTypeFilter
* @see org.springframework.beans.factory.BeanClassLoaderAware
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.context.EnvironmentAware
* @see org.springframework.context.ResourceLoaderAware
*/
public static List<TypeFilter> createTypeFiltersFor(AnnotationAttributes filterAttributes, Environment environment,
ResourceLoader resourceLoader, BeanDefinitionRegistry registry) {
List<TypeFilter> typeFilters = new ArrayList<>();
FilterType filterType = filterAttributes.getEnum("type");
for (Class<?> filterClass : filterAttributes.getClassArray("classes")) {
switch (filterType) {
case ANNOTATION:
Assert.isAssignable(Annotation.class, filterClass,
"@ComponentScan ANNOTATION type filter requires an annotation type");
@SuppressWarnings("unchecked")
Class<Annotation> annotationType = (Class<Annotation>) filterClass;
typeFilters.add(new AnnotationTypeFilter(annotationType));
break;
case ASSIGNABLE_TYPE:
typeFilters.add(new AssignableTypeFilter(filterClass));
break;
case CUSTOM:
Assert.isAssignable(TypeFilter.class, filterClass,
"@ComponentScan CUSTOM type filter requires a TypeFilter implementation");
TypeFilter filter = ParserStrategyUtils.instantiateClass(filterClass, TypeFilter.class,
environment, resourceLoader, registry);
typeFilters.add(filter);
break;
default:
throw new IllegalArgumentException("Filter type not supported with Class value: " + filterType);
}
}
for (String expression : filterAttributes.getStringArray("pattern")) {
switch (filterType) {
case ASPECTJ:
typeFilters.add(new AspectJTypeFilter(expression, resourceLoader.getClassLoader()));
break;
case REGEX:
typeFilters.add(new RegexPatternTypeFilter(Pattern.compile(expression)));
break;
default:
throw new IllegalArgumentException("Filter type not supported with String pattern: " + filterType);
}
}
return typeFilters;
}
}
@@ -75,7 +75,7 @@ public abstract class CachedExpressionEvaluator {
/**
* Return the {@link Expression} for the specified SpEL value
* <p>Parse the expression if it hasn't been already.
* <p>{@link #parseExpression(String) Parse the expression} if it hasn't been already.
* @param cache the cache to use
* @param elementKey the element on which the expression is defined
* @param expression the expression to parse
@@ -86,12 +86,21 @@ public abstract class CachedExpressionEvaluator {
ExpressionKey expressionKey = createKey(elementKey, expression);
Expression expr = cache.get(expressionKey);
if (expr == null) {
expr = getParser().parseExpression(expression);
expr = parseExpression(expression);
cache.put(expressionKey, expr);
}
return expr;
}
/**
* Parse the specified {@code expression}.
* @param expression the expression to parse
* @since 5.3.13
*/
protected Expression parseExpression(String expression) {
return getParser().parseExpression(expression);
}
private ExpressionKey createKey(AnnotatedElementKey elementKey, String expression) {
return new ExpressionKey(elementKey, expression);
}
@@ -107,7 +107,7 @@ public final class CandidateComponentsIndexLoader {
result.add(properties);
}
if (logger.isDebugEnabled()) {
logger.debug("Loaded " + result.size() + "] index(es)");
logger.debug("Loaded " + result.size() + " index(es)");
}
int totalCount = result.stream().mapToInt(Properties::size).sum();
return (totalCount > 0 ? new CandidateComponentsIndex(result) : null);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -67,6 +67,17 @@ public class MapAccessorTests {
assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar");
assertThat(SpelCompiler.compile(ex)).isTrue();
assertThat(ex.getValue(sec,mapGetter)).isEqualTo("bar");
// basic isWritable
ex = sep.parseExpression("foo");
assertThat(ex.isWritable(sec,testMap)).isTrue();
// basic write
ex = sep.parseExpression("foo2");
ex.setValue(sec, testMap, "bar2");
assertThat(ex.getValue(sec,testMap)).isEqualTo("bar2");
assertThat(SpelCompiler.compile(ex)).isTrue();
assertThat(ex.getValue(sec,testMap)).isEqualTo("bar2");
}
public static class MapGetter {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -321,28 +321,6 @@ public class FormattingConversionServiceTests {
TypeDescriptor.valueOf(String.class));
}
@Test
public void registerDefaultValueViaFormatter() {
registerDefaultValue(Date.class, new Date());
}
private <T> void registerDefaultValue(Class<T> clazz, final T defaultValue) {
formattingService.addFormatterForFieldType(clazz, new Formatter<T>() {
@Override
public T parse(String text, Locale locale) {
return defaultValue;
}
@Override
public String print(T t, Locale locale) {
return defaultValue.toString();
}
@Override
public String toString() {
return defaultValue.toString();
}
});
}
@Test
public void introspectedFormatter() {
formattingService.addFormatter(new NumberStyleFormatter("#,#00.0#"));
@@ -37,7 +37,7 @@ import org.springframework.util.ObjectUtils;
/**
* Contextual descriptor about a type to convert from or to.
* Capable of representing arrays and generic collection types.
* <p>Capable of representing arrays and generic collection types.
*
* @author Keith Donald
* @author Andy Clement
@@ -345,9 +345,9 @@ public class TypeDescriptor implements Serializable {
* from the provided collection or array element.
* <p>Narrows the {@link #getElementTypeDescriptor() elementType} property to the class
* of the provided collection or array element. For example, if this describes a
* {@code java.util.List&lt;java.lang.Number&lt;} and the element argument is an
* {@code java.util.List<java.lang.Number>} and the element argument is a
* {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}.
* If this describes a {@code java.util.List&lt;?&gt;} and the element argument is an
* If this describes a {@code java.util.List<?>} and the element argument is a
* {@code java.lang.Integer}, the returned TypeDescriptor will be {@code java.lang.Integer}
* as well.
* <p>Annotation and nested type context will be preserved in the narrowed
@@ -388,9 +388,9 @@ public class TypeDescriptor implements Serializable {
* from the provided map key.
* <p>Narrows the {@link #getMapKeyTypeDescriptor() mapKeyType} property
* to the class of the provided map key. For example, if this describes a
* {@code java.util.Map&lt;java.lang.Number, java.lang.String&lt;} and the key
* {@code java.util.Map<java.lang.Number, java.lang.String>} and the key
* argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be
* {@code java.lang.Integer}. If this describes a {@code java.util.Map&lt;?, ?&gt;}
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
* and the key argument is a {@code java.lang.Integer}, the returned
* TypeDescriptor will be {@code java.lang.Integer} as well.
* <p>Annotation and nested type context will be preserved in the narrowed
@@ -425,9 +425,9 @@ public class TypeDescriptor implements Serializable {
* from the provided map value.
* <p>Narrows the {@link #getMapValueTypeDescriptor() mapValueType} property
* to the class of the provided map value. For example, if this describes a
* {@code java.util.Map&lt;java.lang.String, java.lang.Number&lt;} and the value
* {@code java.util.Map<java.lang.String, java.lang.Number>} and the value
* argument is a {@code java.lang.Integer}, the returned TypeDescriptor will be
* {@code java.lang.Integer}. If this describes a {@code java.util.Map&lt;?, ?&gt;}
* {@code java.lang.Integer}. If this describes a {@code java.util.Map<?, ?>}
* and the value argument is a {@code java.lang.Integer}, the returned
* TypeDescriptor will be {@code java.lang.Integer} as well.
* <p>Annotation and nested type context will be preserved in the narrowed
@@ -36,8 +36,9 @@ import org.springframework.lang.Nullable;
public abstract class LogFormatUtils {
/**
* Variant of {@link #formatValue(Object, int, boolean)} and a convenience
* method that truncates at 100 characters when {@code limitLength} is set.
* Convenience variant of {@link #formatValue(Object, int, boolean)} that
* limits the length of a log message to 100 characters and also replaces
* newline characters if {@code limitLength} is set to "true".
* @param value the value to format
* @param limitLength whether to truncate the value at a length of 100
* @return the formatted value
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2017 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -44,7 +44,7 @@ public interface TypeConverter {
* Convert (or coerce) a value from one type to another, for example from a
* {@code boolean} to a {@code String}.
* <p>The {@link TypeDescriptor} parameters enable support for typed collections:
* A caller may prefer a {@code List&lt;Integer&gt;}, for example, rather than
* A caller may prefer a {@code List<Integer>}, for example, rather than
* simply any {@code List}.
* @param value the value to be converted
* @param sourceType a type descriptor that supplies extra information about the
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2018 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ import org.springframework.util.MethodInvoker;
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public abstract class ReflectionHelper {
@@ -281,25 +282,32 @@ public abstract class ReflectionHelper {
arguments[i] = converter.convertValue(argument, TypeDescriptor.forObject(argument), targetType);
conversionOccurred |= (argument != arguments[i]);
}
MethodParameter methodParam = MethodParameter.forExecutable(executable, varargsPosition);
// If the target is varargs and there is just one more argument, then convert it here.
if (varargsPosition == arguments.length - 1) {
// If the target is varargs and there is just one more argument
// then convert it here
TypeDescriptor targetType = new TypeDescriptor(methodParam);
Object argument = arguments[varargsPosition];
TypeDescriptor targetType = new TypeDescriptor(methodParam);
TypeDescriptor sourceType = TypeDescriptor.forObject(argument);
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
// Three outcomes of that previous line:
// 1) the input argument was already compatible (ie. array of valid type) and nothing was done
// 2) the input argument was correct type but not in an array so it was made into an array
// 3) the input argument was the wrong type and got converted and put into an array
// If the argument type is equal to the varargs element type, there is no need
// to convert it or wrap it in an array. For example, using StringToArrayConverter
// to convert a String containing a comma would result in the String being split
// and repackaged in an array when it should be used as-is.
if (!sourceType.equals(targetType.getElementTypeDescriptor())) {
arguments[varargsPosition] = converter.convertValue(argument, sourceType, targetType);
}
// Three outcomes of the above if-block:
// 1) the input argument was correct type but not wrapped in an array, and nothing was done.
// 2) the input argument was already compatible (i.e., array of valid type), and nothing was done.
// 3) the input argument was the wrong type and got converted and wrapped in an array.
if (argument != arguments[varargsPosition] &&
!isFirstEntryInArray(argument, arguments[varargsPosition])) {
conversionOccurred = true; // case 3
}
}
// Otherwise, convert remaining arguments to the varargs element type.
else {
// Convert remaining arguments to the varargs element type
TypeDescriptor targetType = new TypeDescriptor(methodParam).getElementTypeDescriptor();
Assert.state(targetType != null, "No element type");
for (int i = varargsPosition; i < arguments.length; i++) {
@@ -332,8 +340,8 @@ public abstract class ReflectionHelper {
}
/**
* Package up the arguments so that they correctly match what is expected in parameterTypes.
* For example, if parameterTypes is {@code (int, String[])} because the second parameter
* Package up the arguments so that they correctly match what is expected in requiredParameterTypes.
* <p>For example, if requiredParameterTypes is {@code (int, String[])} because the second parameter
* was declared {@code String...}, then if arguments is {@code [1,"a","b"]} then it must be
* repackaged as {@code [1,new String[]{"a","b"}]} in order to match the expected types.
* @param requiredParameterTypes the types of the parameters for the invocation
@@ -350,23 +358,24 @@ public abstract class ReflectionHelper {
requiredParameterTypes[parameterCount - 1] !=
(args[argumentCount - 1] != null ? args[argumentCount - 1].getClass() : null)) {
int arraySize = 0; // zero size array if nothing to pass as the varargs parameter
if (argumentCount >= parameterCount) {
arraySize = argumentCount - (parameterCount - 1);
}
// Create an array for the varargs arguments
// Create an array for the leading arguments plus the varargs array argument.
Object[] newArgs = new Object[parameterCount];
// Copy all leading arguments to the new array, omitting the varargs array argument.
System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1);
// Now sort out the final argument, which is the varargs one. Before entering this method,
// the arguments should have been converted to the box form of the required type.
Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
Object repackagedArgs = Array.newInstance(componentType, arraySize);
for (int i = 0; i < arraySize; i++) {
Array.set(repackagedArgs, i, args[parameterCount - 1 + i]);
int varargsArraySize = 0; // zero size array if nothing to pass as the varargs parameter
if (argumentCount >= parameterCount) {
varargsArraySize = argumentCount - (parameterCount - 1);
}
newArgs[newArgs.length - 1] = repackagedArgs;
Class<?> componentType = requiredParameterTypes[parameterCount - 1].getComponentType();
Object varargsArray = Array.newInstance(componentType, varargsArraySize);
for (int i = 0; i < varargsArraySize; i++) {
Array.set(varargsArray, i, args[parameterCount - 1 + i]);
}
// Finally, add the varargs array to the new arguments array.
newArgs[newArgs.length - 1] = varargsArray;
return newArgs;
}
return args;
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -46,6 +46,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
*
* @author Andy Clement
* @author Phillip Webb
* @author Sam Brannen
*/
public class MethodInvocationTests extends AbstractExpressionTests {
@@ -233,26 +234,54 @@ public class MethodInvocationTests extends AbstractExpressionTests {
@Test
public void testVarargsInvocation01() {
// Calling 'public int aVarargsMethod(String... strings)'
//evaluate("aVarargsMethod('a','b','c')", 3, Integer.class);
//evaluate("aVarargsMethod('a')", 1, Integer.class);
// Calling 'public int aVarargsMethod(String... strings)' - returns number of arguments
evaluate("aVarargsMethod('a','b','c')", 3, Integer.class);
evaluate("aVarargsMethod('a')", 1, Integer.class);
evaluate("aVarargsMethod()", 0, Integer.class);
evaluate("aVarargsMethod(1,2,3)", 3, Integer.class); // all need converting to strings
evaluate("aVarargsMethod(1)", 1, Integer.class); // needs string conversion
evaluate("aVarargsMethod(1,'a',3.0d)", 3, Integer.class); // first and last need conversion
// evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class);
evaluate("aVarargsMethod(new String[]{'a','b','c'})", 3, Integer.class);
}
@Test
public void testVarargsInvocation02() {
// Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int+length_of_strings
// Calling 'public int aVarargsMethod2(int i, String... strings)' - returns int + length_of_strings
evaluate("aVarargsMethod2(5,'a','b','c')", 8, Integer.class);
evaluate("aVarargsMethod2(2,'a')", 3, Integer.class);
evaluate("aVarargsMethod2(4)", 4, Integer.class);
evaluate("aVarargsMethod2(8,2,3)", 10, Integer.class);
evaluate("aVarargsMethod2(9)", 9, Integer.class);
evaluate("aVarargsMethod2(2,'a',3.0d)", 4, Integer.class);
// evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class);
evaluate("aVarargsMethod2(8,new String[]{'a','b','c'})", 11, Integer.class);
}
@Test
public void testVarargsInvocation03() {
// Calling 'public int aVarargsMethod3(String str1, String... strings)' - returns all strings concatenated with "-"
// No conversion necessary
evaluate("aVarargsMethod3('x')", "x", String.class);
evaluate("aVarargsMethod3('x', 'a')", "x-a", String.class);
evaluate("aVarargsMethod3('x', 'a', 'b', 'c')", "x-a-b-c", String.class);
// Conversion necessary
evaluate("aVarargsMethod3(9)", "9", String.class);
evaluate("aVarargsMethod3(8,2,3)", "8-2-3", String.class);
evaluate("aVarargsMethod3('2','a',3.0d)", "2-a-3.0", String.class);
evaluate("aVarargsMethod3('8',new String[]{'a','b','c'})", "8-a-b-c", String.class);
// Individual string contains a comma with multiple varargs arguments
evaluate("aVarargsMethod3('foo', ',', 'baz')", "foo-,-baz", String.class);
evaluate("aVarargsMethod3('foo', 'bar', ',baz')", "foo-bar-,baz", String.class);
evaluate("aVarargsMethod3('foo', 'bar,', 'baz')", "foo-bar,-baz", String.class);
// Individual string contains a comma with single varargs argument.
// Reproduces https://github.com/spring-projects/spring-framework/issues/27582
evaluate("aVarargsMethod3('foo', ',')", "foo-,", String.class);
evaluate("aVarargsMethod3('foo', ',bar')", "foo-,bar", String.class);
evaluate("aVarargsMethod3('foo', 'bar,')", "foo-bar,", String.class);
evaluate("aVarargsMethod3('foo', 'bar,baz')", "foo-bar,baz", String.class);
}
@Test
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2019 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
* Tests the evaluation of expressions that access variables and functions (lambda/java).
*
* @author Andy Clement
* @author Sam Brannen
*/
public class VariableAndFunctionTests extends AbstractExpressionTests {
@@ -58,12 +59,17 @@ public class VariableAndFunctionTests extends AbstractExpressionTests {
@Test
public void testCallVarargsFunction() {
evaluate("#varargsFunctionReverseStringsAndMerge('a,b')", "a,b", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge('a', 'b,c', 'd')", "db,ca", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge('a','b','c')", "cba", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge('a')", "a", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge()", "", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge('b',25)", "25b", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge(25)", "25", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge2(1, 'a,b')", "1a,b", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge2(1,'a','b','c')", "1cba", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge2(1, 'a', 'b,c', 'd')", "1db,ca", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge2(2,'a')", "2a", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge2(3)", "3", String.class);
evaluate("#varargsFunctionReverseStringsAndMerge2(4,'b',25)", "425b", String.class);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@ import org.springframework.util.ObjectUtils;
///CLOVER:OFF
@SuppressWarnings("unused")
public class Inventor {
private String name;
public String _name;
public String _name_;
@@ -202,8 +203,14 @@ public class Inventor {
return strings.length + i;
}
public Inventor(String... strings) {
public String aVarargsMethod3(String str1, String... strings) {
if (ObjectUtils.isEmpty(strings)) {
return str1;
}
return str1 + "-" + String.join("-", strings);
}
public Inventor(String... strings) {
}
public boolean getSomeProperty() {
@@ -230,10 +230,10 @@ public abstract class ExtendedEntityManagerCreator {
if (emIfc != null) {
interfaces = cachedEntityManagerInterfaces.computeIfAbsent(emIfc, key -> {
Set<Class<?>> ifcs = new LinkedHashSet<>(4);
ifcs.add(key);
ifcs.add(EntityManagerProxy.class);
return ClassUtils.toClassArray(ifcs);
if (EntityManagerProxy.class.equals(key)) {
return new Class<?>[] {key};
}
return new Class<?>[] {key, EntityManagerProxy.class};
});
}
else {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -373,7 +373,7 @@ public class PersistenceAnnotationBeanPostProcessor
}
private InjectionMetadata findPersistenceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
private InjectionMetadata findPersistenceMetadata(String beanName, Class<?> clazz, @Nullable PropertyValues pvs) {
// Fall back to class name as cache key, for backwards compatibility with custom callers.
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
// Quick check on the concurrent map first, with minimal locking.
@@ -393,7 +393,7 @@ public class PersistenceAnnotationBeanPostProcessor
return metadata;
}
private InjectionMetadata buildPersistenceMetadata(final Class<?> clazz) {
private InjectionMetadata buildPersistenceMetadata(Class<?> clazz) {
if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(PersistenceContext.class, PersistenceUnit.class))) {
return InjectionMetadata.EMPTY;
}
@@ -17,6 +17,7 @@
package org.springframework.test.web.reactive.server;
import java.time.Duration;
import java.util.Objects;
import java.util.function.Consumer;
import org.hamcrest.Matcher;
@@ -210,10 +211,10 @@ public class CookieAssertions {
private ResponseCookie getCookie(String name) {
ResponseCookie cookie = this.exchangeResult.getResponseCookies().getFirst(name);
if (cookie == null) {
String message = "No cookie with name '" + name + "'";
this.exchangeResult.assertWithDiagnostics(() -> AssertionErrors.fail(message));
this.exchangeResult.assertWithDiagnostics(() ->
AssertionErrors.fail("No cookie with name '" + name + "'"));
}
return cookie;
return Objects.requireNonNull(cookie);
}
private String getMessage(String cookie) {
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@ package org.springframework.test.web.reactive.server;
import java.net.URI;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import org.hamcrest.Matcher;
@@ -73,7 +74,7 @@ public class HeaderAssertions {
String actual = getHeaders().getFirst(headerName);
this.exchangeResult.assertWithDiagnostics(() ->
assertTrue("Response does not contain header '" + headerName + "'", actual != null));
return assertHeader(headerName, value, Long.parseLong(actual));
return assertHeader(headerName, value, Long.parseLong(Objects.requireNonNull(actual)));
}
/**
@@ -115,7 +116,7 @@ public class HeaderAssertions {
/**
* Match all values of the response header with the given regex
* patterns which are applied to the values of the header in the
* same order. Note that the number of pattenrs must match the
* same order. Note that the number of patterns must match the
* number of actual values.
* @param name the header name
* @param patterns one or more regex patterns, one per expected value
@@ -203,7 +204,7 @@ public class HeaderAssertions {
this.exchangeResult.assertWithDiagnostics(() ->
AssertionErrors.fail(getMessage(name) + " not found"));
}
return values;
return Objects.requireNonNull(values);
}
/**
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2015 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -38,7 +38,6 @@ import org.springframework.util.Assert;
* PersistenceExceptionTranslator} interface, which are subsequently asked to translate
* candidate exceptions.
*
* <p>All of Spring's applicable resource factories (e.g.
* {@link org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean})
* implement the {@code PersistenceExceptionTranslator} interface out of the box.
@@ -47,6 +46,11 @@ import org.springframework.util.Assert;
* with the {@code @Repository} annotation, along with defining this post-processor
* as a bean in the application context.
*
* <p>As of 5.3, {@code PersistenceExceptionTranslator} beans will be sorted according
* to Spring's dependency ordering rules: see {@link org.springframework.core.Ordered}
* and {@link org.springframework.core.annotation.Order}. Note that such beans will
* get retrieved from any scope, not just singleton scope, as of this 5.3 revision.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @since 2.0
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -23,12 +23,12 @@ import org.springframework.util.ObjectUtils;
/**
* Represents an HTTP request or response entity, consisting of headers and body.
*
* <p>Typically used in combination with the {@link org.springframework.web.client.RestTemplate},
* <p>Often used in combination with the {@link org.springframework.web.client.RestTemplate},
* like so:
* <pre class="code">
* HttpHeaders headers = new HttpHeaders();
* headers.setContentType(MediaType.TEXT_PLAIN);
* HttpEntity&lt;String&gt; entity = new HttpEntity&lt;String&gt;(helloWorld, headers);
* HttpEntity&lt;String&gt; entity = new HttpEntity&lt;&gt;("Hello World", headers);
* URI location = template.postForLocation("https://example.com", entity);
* </pre>
* or
@@ -39,11 +39,11 @@ import org.springframework.util.ObjectUtils;
* </pre>
* Can also be used in Spring MVC, as a return value from a @Controller method:
* <pre class="code">
* &#64;RequestMapping("/handle")
* &#64;GetMapping("/handle")
* public HttpEntity&lt;String&gt; handle() {
* HttpHeaders responseHeaders = new HttpHeaders();
* responseHeaders.set("MyResponseHeader", "MyValue");
* return new HttpEntity&lt;String&gt;("Hello World", responseHeaders);
* return new HttpEntity&lt;&gt;("Hello World", responseHeaders);
* }
* </pre>
*
@@ -63,7 +63,7 @@ public class DefaultPartHttpMessageReader extends LoggingCodecSupport implements
private int maxInMemorySize = 256 * 1024;
private int maxHeadersSize = 8 * 1024;
private int maxHeadersSize = 10 * 1024;
private long maxDiskUsagePerPart = -1;
@@ -16,10 +16,15 @@
package org.springframework.http.codec.multipart;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Callable;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Scheduler;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
@@ -50,17 +55,40 @@ abstract class DefaultParts {
}
/**
* Create a new {@link Part} or {@link FilePart} with the given parameters.
* Create a new {@link Part} or {@link FilePart} based on a flux of data
* buffers. Returns {@link FilePart} if the {@code Content-Disposition} of
* the given headers contains a filename, or a "normal" {@link Part}
* otherwise.
* @param headers the part headers
* @param dataBuffers the content of the part
* @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()}
*/
public static Part part(HttpHeaders headers, Flux<DataBuffer> dataBuffers) {
Assert.notNull(headers, "Headers must not be null");
Assert.notNull(dataBuffers, "DataBuffers must not be null");
return partInternal(headers, new FluxContent(dataBuffers));
}
/**
* Create a new {@link Part} or {@link FilePart} based on the given file.
* Returns {@link FilePart} if the {@code Content-Disposition} of the given
* headers contains a filename, or a "normal" {@link Part} otherwise
* @param headers the part headers
* @param content the content of the part
* @param file the file
* @param scheduler the scheduler used for reading the file
* @return {@link Part} or {@link FilePart}, depending on {@link HttpHeaders#getContentDisposition()}
*/
public static Part part(HttpHeaders headers, Flux<DataBuffer> content) {
public static Part part(HttpHeaders headers, Path file, Scheduler scheduler) {
Assert.notNull(headers, "Headers must not be null");
Assert.notNull(content, "Content must not be null");
Assert.notNull(file, "File must not be null");
Assert.notNull(scheduler, "Scheduler must not be null");
return partInternal(headers, new FileContent(file, scheduler));
}
private static Part partInternal(HttpHeaders headers, Content content) {
String filename = headers.getContentDisposition().getFilename();
if (filename != null) {
return new DefaultFilePart(headers, content);
@@ -142,16 +170,22 @@ abstract class DefaultParts {
*/
private static class DefaultPart extends AbstractPart {
private final Flux<DataBuffer> content;
protected final Content content;
public DefaultPart(HttpHeaders headers, Flux<DataBuffer> content) {
public DefaultPart(HttpHeaders headers, Content content) {
super(headers);
this.content = content;
}
@Override
public Flux<DataBuffer> content() {
return this.content;
return this.content.content();
}
@Override
public Mono<Void> delete() {
return this.content.delete();
}
@Override
@@ -171,9 +205,9 @@ abstract class DefaultParts {
/**
* Default implementation of {@link FilePart}.
*/
private static class DefaultFilePart extends DefaultPart implements FilePart {
private static final class DefaultFilePart extends DefaultPart implements FilePart {
public DefaultFilePart(HttpHeaders headers, Flux<DataBuffer> content) {
public DefaultFilePart(HttpHeaders headers, Content content) {
super(headers, content);
}
@@ -186,7 +220,7 @@ abstract class DefaultParts {
@Override
public Mono<Void> transferTo(Path dest) {
return DataBufferUtils.write(content(), dest);
return this.content.transferTo(dest);
}
@Override
@@ -195,7 +229,7 @@ abstract class DefaultParts {
String name = contentDisposition.getName();
String filename = contentDisposition.getFilename();
if (name != null) {
return "DefaultFilePart{" + name() + " (" + filename + ")}";
return "DefaultFilePart{" + name + " (" + filename + ")}";
}
else {
return "DefaultFilePart{(" + filename + ")}";
@@ -204,4 +238,100 @@ abstract class DefaultParts {
}
/**
* Part content abstraction.
*/
private interface Content {
Flux<DataBuffer> content();
Mono<Void> transferTo(Path dest);
Mono<Void> delete();
}
/**
* {@code Content} implementation based on a flux of data buffers.
*/
private static final class FluxContent implements Content {
private final Flux<DataBuffer> content;
public FluxContent(Flux<DataBuffer> content) {
this.content = content;
}
@Override
public Flux<DataBuffer> content() {
return this.content;
}
@Override
public Mono<Void> transferTo(Path dest) {
return DataBufferUtils.write(this.content, dest);
}
@Override
public Mono<Void> delete() {
return Mono.empty();
}
}
/**
* {@code Content} implementation based on a file.
*/
private static final class FileContent implements Content {
private final Path file;
private final Scheduler scheduler;
public FileContent(Path file, Scheduler scheduler) {
this.file = file;
this.scheduler = scheduler;
}
@Override
public Flux<DataBuffer> content() {
return DataBufferUtils.readByteChannel(
() -> Files.newByteChannel(this.file, StandardOpenOption.READ),
DefaultDataBufferFactory.sharedInstance, 1024)
.subscribeOn(this.scheduler);
}
@Override
public Mono<Void> transferTo(Path dest) {
return blockingOperation(() -> Files.copy(this.file, dest, StandardCopyOption.REPLACE_EXISTING));
}
@Override
public Mono<Void> delete() {
return blockingOperation(() -> {
Files.delete(this.file);
return null;
});
}
private Mono<Void> blockingOperation(Callable<?> callable) {
return Mono.<Void>create(sink -> {
try {
callable.call();
sink.success();
}
catch (Exception ex) {
sink.error(ex);
}
})
.subscribeOn(this.scheduler);
}
}
}
@@ -342,33 +342,23 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
/**
* First checks whether the multipart boundary leading to this state
* was the final boundary, or whether {@link #maxHeadersSize} is
* exceeded. Then looks for the header-body boundary
* ({@code CR LF CR LF}) in the given buffer. If found, convert
* all buffers collected so far into a {@link HttpHeaders} object
* was the final boundary. Then looks for the header-body boundary
* ({@code CR LF CR LF}) in the given buffer. If found, checks whether
* the size of all header buffers does not exceed {@link #maxHeadersSize},
* converts all buffers collected so far into a {@link HttpHeaders} object
* and changes to {@link BodyState}, passing the remainder of the
* buffer. If the boundary is not found, the buffer is collected.
* buffer. If the boundary is not found, the buffer is collected if
* its size does not exceed {@link #maxHeadersSize}.
*/
@Override
public void onNext(DataBuffer buf) {
long prevCount = this.byteCount.get();
long count = this.byteCount.addAndGet(buf.readableByteCount());
if (prevCount < 2 && count >= 2) {
if (isLastBoundary(buf)) {
if (logger.isTraceEnabled()) {
logger.trace("Last boundary found in " + buf);
}
if (changeState(this, DisposedState.INSTANCE, buf)) {
emitComplete();
}
return;
if (isLastBoundary(buf)) {
if (logger.isTraceEnabled()) {
logger.trace("Last boundary found in " + buf);
}
}
else if (count > MultipartParser.this.maxHeadersSize) {
if (changeState(this, DisposedState.INSTANCE, buf)) {
emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " +
MultipartParser.this.maxHeadersSize + " bytes"));
emitComplete();
}
return;
}
@@ -377,17 +367,23 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
if (logger.isTraceEnabled()) {
logger.trace("End of headers found @" + endIdx + " in " + buf);
}
DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx);
this.buffers.add(headerBuf);
DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx);
DataBufferUtils.release(buf);
long count = this.byteCount.addAndGet(endIdx);
if (belowMaxHeaderSize(count)) {
DataBuffer headerBuf = MultipartUtils.sliceTo(buf, endIdx);
this.buffers.add(headerBuf);
DataBuffer bodyBuf = MultipartUtils.sliceFrom(buf, endIdx);
DataBufferUtils.release(buf);
emitHeaders(parseHeaders());
changeState(this, new BodyState(), bodyBuf);
emitHeaders(parseHeaders());
changeState(this, new BodyState(), bodyBuf);
}
}
else {
this.buffers.add(buf);
requestBuffer();
long count = this.byteCount.addAndGet(buf.readableByteCount());
if (belowMaxHeaderSize(count)) {
this.buffers.add(buf);
requestBuffer();
}
}
}
@@ -407,6 +403,21 @@ final class MultipartParser extends BaseSubscriber<DataBuffer> {
buf.getByte(0) == HYPHEN);
}
/**
* Checks whether the given {@code count} is below or equal to {@link #maxHeadersSize}
* and emits a {@link DataBufferLimitException} if not.
*/
private boolean belowMaxHeaderSize(long count) {
if (count <= MultipartParser.this.maxHeadersSize) {
return true;
}
else {
emitError(new DataBufferLimitException("Part headers exceeded the memory usage limit of " +
MultipartParser.this.maxHeadersSize + " bytes"));
return false;
}
}
/**
* Parses the list of buffers into a {@link HttpHeaders} instance.
* Converts the joined buffers into a string using ISO=8859-1, and parses
@@ -17,6 +17,7 @@
package org.springframework.http.codec.multipart;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
@@ -57,4 +58,13 @@ public interface Part {
*/
Flux<DataBuffer> content();
/**
* Return a mono that, when subscribed to, deletes the underlying storage
* for this part.
* @since 5.3.13
*/
default Mono<Void> delete() {
return Mono.empty();
}
}
@@ -674,21 +674,12 @@ final class PartGenerator extends BaseSubscriber<MultipartParser.Token> {
@Override
public void partComplete(boolean finalPart) {
MultipartUtils.closeChannel(this.channel);
Flux<DataBuffer> content = partContent();
emitPart(DefaultParts.part(this.headers, content));
emitPart(DefaultParts.part(this.headers, this.file, PartGenerator.this.blockingOperationScheduler));
if (finalPart) {
emitComplete();
}
}
private Flux<DataBuffer> partContent() {
return DataBufferUtils
.readByteChannel(
() -> Files.newByteChannel(this.file, StandardOpenOption.READ),
DefaultDataBufferFactory.sharedInstance, 1024)
.subscribeOn(PartGenerator.this.blockingOperationScheduler);
}
@Override
public void dispose() {
if (this.closeOnDispose) {
@@ -16,7 +16,9 @@
package org.springframework.http.codec.multipart;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
@@ -40,6 +42,7 @@ import org.synchronoss.cloud.nio.multipart.MultipartUtils;
import org.synchronoss.cloud.nio.multipart.NioMultipartParser;
import org.synchronoss.cloud.nio.multipart.NioMultipartParserListener;
import org.synchronoss.cloud.nio.multipart.PartBodyStreamStorageFactory;
import org.synchronoss.cloud.nio.stream.storage.NameAwarePurgableFileInputStream;
import org.synchronoss.cloud.nio.stream.storage.StreamStorage;
import reactor.core.publisher.BaseSubscriber;
import reactor.core.publisher.Flux;
@@ -497,6 +500,38 @@ public class SynchronossPartHttpMessageReader extends LoggingCodecSupport implem
protected StreamStorage getStorage() {
return this.storage;
}
@Override
public Mono<Void> delete() {
return Mono.fromRunnable(() -> {
File file = getFile();
if (file != null) {
file.delete();
}
});
}
@Nullable
private File getFile() {
InputStream inputStream = null;
try {
inputStream = getStorage().getInputStream();
if (inputStream instanceof NameAwarePurgableFileInputStream) {
NameAwarePurgableFileInputStream stream = (NameAwarePurgableFileInputStream) inputStream;
return stream.getFile();
}
}
finally {
if (inputStream != null) {
try {
inputStream.close();
}
catch (IOException ignore) {
}
}
}
return null;
}
}
@@ -17,6 +17,7 @@
package org.springframework.http.server.reactive;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
@@ -36,6 +37,7 @@ import org.springframework.util.MultiValueMap;
* {@code MultiValueMap} implementation for wrapping Undertow HTTP headers.
*
* @author Brian Clozel
* @author Sam Brannen
* @since 5.1.1
*/
class UndertowHeadersAdapter implements MultiValueMap<String, String> {
@@ -131,7 +133,10 @@ class UndertowHeadersAdapter implements MultiValueMap<String, String> {
@Nullable
public List<String> remove(Object key) {
if (key instanceof String) {
this.headers.remove((String) key);
Collection<String> removed = this.headers.remove((String) key);
if (removed != null) {
return new ArrayList<>(removed);
}
}
return null;
}
@@ -405,16 +405,18 @@ public class UrlPathHelper {
* </ul>
*/
private static String getSanitizedPath(final String path) {
int index = path.indexOf("//");
if (index >= 0) {
StringBuilder sanitized = new StringBuilder(path);
while (index != -1) {
sanitized.deleteCharAt(index);
index = sanitized.indexOf("//", index);
}
return sanitized.toString();
int start = path.indexOf("//");
if (start == -1) {
return path;
}
return path;
char[] content = path.toCharArray();
int slowIndex = start;
for (int fastIndex = start + 1; fastIndex < content.length; fastIndex++) {
if (content[fastIndex] != '/' || content[slowIndex] != '/') {
content[++slowIndex] = content[fastIndex];
}
}
return new String(content, 0, slowIndex + 1);
}
/**
@@ -532,7 +534,7 @@ public class UrlPathHelper {
*/
public String getOriginatingQueryString(HttpServletRequest request) {
if ((request.getAttribute(WebUtils.FORWARD_REQUEST_URI_ATTRIBUTE) != null) ||
(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) {
(request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null)) {
return (String) request.getAttribute(WebUtils.FORWARD_QUERY_STRING_ATTRIBUTE);
}
else {
@@ -270,6 +270,31 @@ public class DefaultPartHttpMessageReaderTests {
latch.await();
}
// gh-27612
@Test
public void exceedHeaderLimit() throws InterruptedException {
Flux<DataBuffer> body = DataBufferUtils
.readByteChannel((new ClassPathResource("files.multipart", getClass()))::readableChannel, bufferFactory, 282);
MediaType contentType = new MediaType("multipart", "form-data", singletonMap("boundary", "----WebKitFormBoundaryG8fJ50opQOML0oGD"));
MockServerHttpRequest request = MockServerHttpRequest.post("/")
.contentType(contentType)
.body(body);
DefaultPartHttpMessageReader reader = new DefaultPartHttpMessageReader();
reader.setMaxHeadersSize(230);
Flux<Part> result = reader.read(forClass(Part.class), request, emptyMap());
CountDownLatch latch = new CountDownLatch(2);
StepVerifier.create(result)
.consumeNextWith(part -> testPart(part, null, LOREM_IPSUM, latch))
.consumeNextWith(part -> testPart(part, null, MUSPI_MEROL, latch))
.verifyComplete();
latch.await();
}
private void testBrowser(DefaultPartHttpMessageReader reader, Resource resource, String boundary)
throws InterruptedException {
@@ -232,12 +232,12 @@ class UrlPathHelperTests {
request.setContextPath("/SPR-12372");
request.setPathInfo(null);
request.setServletPath("/foo/bar/");
request.setRequestURI("/SPR-12372/foo//bar/");
request.setRequestURI("/SPR-12372/foo///bar/");
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar/");
request.setServletPath("/foo/bar/");
request.setRequestURI("/SPR-12372/foo/bar//");
request.setRequestURI("////SPR-12372/foo/bar//");
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar/");
@@ -246,6 +246,12 @@ class UrlPathHelperTests {
request.setRequestURI("/SPR-12372/foo/bar//");
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar//");
// "enhance" case
request.setServletPath("/foo/bar//");
request.setRequestURI("/SPR-12372////////////////////////foo//////////////////bar////////////////////");
assertThat(helper.getLookupPathForRequest(request)).isEqualTo("/foo/bar//");
}
@Test
@@ -3,11 +3,9 @@ Content-Disposition: form-data; name="file2"; filename="a.txt"
Content-Type: text/plain
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer iaculis metus id vestibulum nullam.
------WebKitFormBoundaryG8fJ50opQOML0oGD
Content-Disposition: form-data; name="file2"; filename="b.txt"
Content-Type: text/plain
.mallun mulubitsev di sutem silucai regetnI .tile gnicsipida rutetcesnoc ,tema tis rolod muspi meroL
------WebKitFormBoundaryG8fJ50opQOML0oGD--
@@ -54,6 +54,8 @@ public class ResourceHandlerRegistration {
private boolean useLastModified = true;
private boolean optimizeLocations = false;
@Nullable
private Map<String, MediaType> mediaTypes;
@@ -105,15 +107,33 @@ public class ResourceHandlerRegistration {
/**
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
* <p>This configuration is set to {@code true} by default.
* @param useLastModified whether the "last modified" resource information should be used.
* @param useLastModified whether the "last modified" resource information should be used
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 5.3
* @see ResourceWebHandler#setUseLastModified
*/
public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
return this;
}
/**
* Set whether to optimize the specified locations through an existence check on startup,
* filtering non-existing directories upfront so that they do not have to be checked
* on every resource access.
* <p>The default is {@code false}, for defensiveness against zip files without directory
* entries which are unable to expose the existence of a directory upfront. Switch this flag to
* {@code true} for optimized access in case of a consistent jar layout with directory entries.
* @param optimizeLocations whether to optimize the locations through an existence check on startup
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 5.3.13
* @see ResourceWebHandler#setOptimizeLocations
*/
public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) {
this.optimizeLocations = optimizeLocations;
return this;
}
/**
* Configure a chain of resource resolvers and transformers to use. This
* can be useful, for example, to apply a version strategy to resource URLs.
@@ -181,8 +201,8 @@ public class ResourceHandlerRegistration {
*/
protected ResourceWebHandler getRequestHandler() {
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocationValues(this.locationValues);
handler.setResourceLoader(this.resourceLoader);
handler.setLocationValues(this.locationValues);
if (this.resourceChainRegistration != null) {
handler.setResourceResolvers(this.resourceChainRegistration.getResourceResolvers());
handler.setResourceTransformers(this.resourceChainRegistration.getResourceTransformers());
@@ -191,6 +211,7 @@ public class ResourceHandlerRegistration {
handler.setCacheControl(this.cacheControl);
}
handler.setUseLastModified(this.useLastModified);
handler.setOptimizeLocations(this.optimizeLocations);
if (this.mediaTypes != null) {
handler.setMediaTypes(this.mediaTypes);
}
@@ -195,13 +195,7 @@ class DefaultClientResponse implements ClientResponse {
@Override
public Mono<WebClientResponseException> createException() {
return DataBufferUtils.join(body(BodyExtractors.toDataBuffers()))
.map(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
DataBufferUtils.release(dataBuffer);
return bytes;
})
return bodyToMono(byte[].class)
.defaultIfEmpty(EMPTY)
.onErrorReturn(ex -> !(ex instanceof Error), EMPTY)
.map(bodyBytes -> {
@@ -72,8 +72,8 @@ import org.springframework.web.server.WebHandler;
* <p>This request handler may also be configured with a
* {@link #setResourceResolvers(List) resourcesResolver} and
* {@link #setResourceTransformers(List) resourceTransformer} chains to support
* arbitrary resolution and transformation of resources being served. By default a
* {@link PathResourceResolver} simply finds resources based on the configured
* arbitrary resolution and transformation of resources being served. By default
* a {@link PathResourceResolver} simply finds resources based on the configured
* "locations". An application can configure additional resolvers and
* transformers such as the {@link VersionResourceResolver} which can resolve
* and prepare URLs for resources with a version in the URL.
@@ -85,6 +85,7 @@ import org.springframework.web.server.WebHandler;
*
* @author Rossen Stoyanchev
* @author Brian Clozel
* @author Juergen Hoeller
* @since 5.0
*/
public class ResourceWebHandler implements WebHandler, InitializingBean {
@@ -94,6 +95,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
private static final Log logger = LogFactory.getLog(ResourceWebHandler.class);
@Nullable
private ResourceLoader resourceLoader;
private final List<String> locationValues = new ArrayList<>(4);
private final List<Resource> locationResources = new ArrayList<>(4);
@@ -119,11 +123,18 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
@Nullable
private Map<String, MediaType> mediaTypes;
@Nullable
private ResourceLoader resourceLoader;
private boolean useLastModified = true;
private boolean optimizeLocations = false;
/**
* Provide the ResourceLoader to load {@link #setLocationValues location values} with.
* @since 5.1
*/
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* Accepts a list of String-based location values to be resolved into
@@ -161,9 +172,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
* <p>Note that if {@link #setLocationValues(List) locationValues} are provided,
* instead of loaded Resource-based locations, this method will return
* empty until after initialization via {@link #afterPropertiesSet()}.
* <p><strong>Note:</strong> As of 5.3.11 the list of locations is filtered
* to exclude those that don't actually exist and therefore the list returned
* from this method may be a subset of all given locations.
* <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
* exclude those that don't actually exist and therefore the list returned from this
* method may be a subset of all given locations. See {@link #setOptimizeLocations}.
* @see #setLocationValues
* @see #setLocations
*/
@@ -212,6 +223,22 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
return this.resourceTransformers;
}
/**
* Configure the {@link ResourceHttpMessageWriter} to use.
* <p>By default a {@link ResourceHttpMessageWriter} will be configured.
*/
public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) {
this.resourceHttpMessageWriter = httpMessageWriter;
}
/**
* Return the configured resource message writer.
*/
@Nullable
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
return this.resourceHttpMessageWriter;
}
/**
* Set the {@link org.springframework.http.CacheControl} instance to build
* the Cache-Control HTTP response header.
@@ -230,19 +257,48 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
}
/**
* Configure the {@link ResourceHttpMessageWriter} to use.
* <p>By default a {@link ResourceHttpMessageWriter} will be configured.
* Set whether we should look at the {@link Resource#lastModified()}
* when serving resources and use this information to drive {@code "Last-Modified"}
* HTTP response headers.
* <p>This option is enabled by default and should be turned off if the metadata of
* the static files should be ignored.
* @since 5.3
*/
public void setResourceHttpMessageWriter(@Nullable ResourceHttpMessageWriter httpMessageWriter) {
this.resourceHttpMessageWriter = httpMessageWriter;
public void setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
}
/**
* Return the configured resource message writer.
* Return whether the {@link Resource#lastModified()} information is used
* to drive HTTP responses when serving static resources.
* @since 5.3
*/
@Nullable
public ResourceHttpMessageWriter getResourceHttpMessageWriter() {
return this.resourceHttpMessageWriter;
public boolean isUseLastModified() {
return this.useLastModified;
}
/**
* Set whether to optimize the specified locations through an existence
* check on startup, filtering non-existing directories upfront so that
* they do not have to be checked on every resource access.
* <p>The default is {@code false}, for defensiveness against zip files
* without directory entries which are unable to expose the existence of
* a directory upfront. Switch this flag to {@code true} for optimized
* access in case of a consistent jar layout with directory entries.
* @since 5.3.13
*/
public void setOptimizeLocations(boolean optimizeLocations) {
this.optimizeLocations = optimizeLocations;
}
/**
* Return whether to optimize the specified locations through an existence
* check on startup, filtering non-existing directories upfront so that
* they do not have to be checked on every resource access.
* @since 5.3.13
*/
public boolean isOptimizeLocations() {
return this.optimizeLocations;
}
/**
@@ -269,36 +325,6 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
return (this.mediaTypes != null ? this.mediaTypes : Collections.emptyMap());
}
/**
* Provide the ResourceLoader to load {@link #setLocationValues(List)
* location values} with.
* @since 5.1
*/
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
/**
* Return whether the {@link Resource#lastModified()} information is used
* to drive HTTP responses when serving static resources.
* @since 5.3
*/
public boolean isUseLastModified() {
return this.useLastModified;
}
/**
* Set whether we should look at the {@link Resource#lastModified()}
* when serving resources and use this information to drive {@code "Last-Modified"}
* HTTP response headers.
* <p>This option is enabled by default and should be turned off if the metadata of
* the static files should be ignored.
* @param useLastModified whether to use the resource last-modified information.
* @since 5.3
*/
public void setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
}
@Override
public void afterPropertiesSet() throws Exception {
@@ -332,7 +358,9 @@ public class ResourceWebHandler implements WebHandler, InitializingBean {
}
}
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
if (isOptimizeLocations()) {
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
}
this.locationsToUse.clear();
this.locationsToUse.addAll(result);
@@ -30,6 +30,7 @@ import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.core.codec.ByteArrayDecoder;
import org.springframework.core.codec.StringDecoder;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DefaultDataBuffer;
@@ -48,6 +49,7 @@ import org.springframework.util.MultiValueMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.entry;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.mock;
import static org.springframework.web.reactive.function.BodyExtractors.toMono;
@@ -328,6 +330,29 @@ public class DefaultClientResponseTests {
assertThat(result.getHeaders().getContentType()).isEqualTo(MediaType.TEXT_PLAIN);
}
@Test
public void createException() {
byte[] bytes = "foo".getBytes(StandardCharsets.UTF_8);
DefaultDataBuffer dataBuffer = DefaultDataBufferFactory.sharedInstance.wrap(ByteBuffer.wrap(bytes));
Flux<DataBuffer> body = Flux.just(dataBuffer);
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
given(mockResponse.getStatusCode()).willReturn(HttpStatus.NOT_FOUND);
given(mockResponse.getRawStatusCode()).willReturn(HttpStatus.NOT_FOUND.value());
given(mockResponse.getBody()).willReturn(body);
List<HttpMessageReader<?>> messageReaders = Collections.singletonList(
new DecoderHttpMessageReader<>(new ByteArrayDecoder()));
given(mockExchangeStrategies.messageReaders()).willReturn(messageReaders);
Mono<WebClientResponseException> resultMono = defaultClientResponse.createException();
WebClientResponseException exception = resultMono.block();
assertThat(exception.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(exception.getMessage()).isEqualTo("404 Not Found");
assertThat(exception.getHeaders()).containsExactly(entry("Content-Type",
Collections.singletonList("text/plain")));
assertThat(exception.getResponseBodyAsByteArray()).isEqualTo(bytes);
}
private void mockTextPlainResponse(Flux<DataBuffer> body) {
httpHeaders.setContentType(MediaType.TEXT_PLAIN);
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -133,6 +133,7 @@ public class DefaultWebClientTests {
}
@Test
@SuppressWarnings("deprecation")
public void contextFromThreadLocal() {
WebClient client = this.builder
.filter((request, next) ->
@@ -74,6 +74,7 @@ public class ResourceWebHandlerTests {
private ResourceWebHandler handler;
@BeforeEach
public void setup() throws Exception {
List<Resource> locations = new ArrayList<>(2);
@@ -253,7 +254,7 @@ public class ResourceWebHandlerTests {
assertResponseBody(exchange, "h1 { color:red; }");
}
@Test // gh-27538
@Test // gh-27538, gh-27624
public void filterNonExistingLocations() throws Exception {
List<Resource> inputLocations = Arrays.asList(
new ClassPathResource("test/", getClass()),
@@ -262,6 +263,7 @@ public class ResourceWebHandlerTests {
ResourceWebHandler handler = new ResourceWebHandler();
handler.setLocations(inputLocations);
handler.setOptimizeLocations(true);
handler.afterPropertiesSet();
List<Resource> actual = handler.getLocations();
@@ -55,6 +55,8 @@ public class ResourceHandlerRegistration {
private boolean useLastModified = true;
private boolean optimizeLocations = false;
/**
* Create a {@link ResourceHandlerRegistration} instance.
@@ -130,15 +132,33 @@ public class ResourceHandlerRegistration {
/**
* Set whether the {@link Resource#lastModified()} information should be used to drive HTTP responses.
* <p>This configuration is set to {@code true} by default.
* @param useLastModified whether the "last modified" resource information should be used.
* @param useLastModified whether the "last modified" resource information should be used
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 5.3
* @see ResourceHttpRequestHandler#setUseLastModified
*/
public ResourceHandlerRegistration setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
return this;
}
/**
* Set whether to optimize the specified locations through an existence check on startup,
* filtering non-existing directories upfront so that they do not have to be checked
* on every resource access.
* <p>The default is {@code false}, for defensiveness against zip files without directory
* entries which are unable to expose the existence of a directory upfront. Switch this flag to
* {@code true} for optimized access in case of a consistent jar layout with directory entries.
* @param optimizeLocations whether to optimize the locations through an existence check on startup
* @return the same {@link ResourceHandlerRegistration} instance, for chained method invocation
* @since 5.3.13
* @see ResourceHttpRequestHandler#setOptimizeLocations
*/
public ResourceHandlerRegistration setOptimizeLocations(boolean optimizeLocations) {
this.optimizeLocations = optimizeLocations;
return this;
}
/**
* Configure a chain of resource resolvers and transformers to use. This
* can be useful, for example, to apply a version strategy to resource URLs.
@@ -204,6 +224,7 @@ public class ResourceHandlerRegistration {
handler.setCacheSeconds(this.cachePeriod);
}
handler.setUseLastModified(this.useLastModified);
handler.setOptimizeLocations(this.optimizeLocations);
return handler;
}
@@ -140,11 +140,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
@Nullable
private UrlPathHelper urlPathHelper;
private boolean useLastModified = true;
private boolean optimizeLocations = false;
@Nullable
private StringValueResolver embeddedValueResolver;
private boolean useLastModified = true;
public ResourceHttpRequestHandler() {
super(HttpMethod.GET.name(), HttpMethod.HEAD.name());
@@ -185,13 +187,13 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/**
* Return the configured {@code List} of {@code Resource} locations including
* both String-based locations provided via
* {@link #setLocationValues(List) setLocationValues} and pre-resolved {@code Resource}
* locations provided via {@link #setLocations(List) setLocations}.
* {@link #setLocationValues(List) setLocationValues} and pre-resolved
* {@code Resource} locations provided via {@link #setLocations(List) setLocations}.
* <p>Note that the returned list is fully initialized only after
* initialization via {@link #afterPropertiesSet()}.
* <p><strong>Note:</strong> As of 5.3.11 the list of locations is filtered
* to exclude those that don't actually exist and therefore the list returned
* from this method may be a subset of all given locations.
* <p><strong>Note:</strong> As of 5.3.11 the list of locations may be filtered to
* exclude those that don't actually exist and therefore the list returned from this
* method may be a subset of all given locations. See {@link #setOptimizeLocations}.
* @see #setLocationValues
* @see #setLocations
*/
@@ -293,7 +295,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/**
* Return the configured content negotiation manager.
* @since 4.3
* @deprecated as of 5.2.4.
* @deprecated as of 5.2.4
*/
@Nullable
@Deprecated
@@ -303,7 +305,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
/**
* Add mappings between file extensions, extracted from the filename of a
* static {@link Resource}, and corresponding media type to set on the
* static {@link Resource}, and corresponding media type to set on the
* response.
* <p>Use of this method is typically not necessary since mappings are
* otherwise determined via
@@ -361,9 +363,16 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return this.urlPathHelper;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
/**
* Set whether we should look at the {@link Resource#lastModified()} when
* serving resources and use this information to drive {@code "Last-Modified"}
* HTTP response headers.
* <p>This option is enabled by default and should be turned off if the metadata
* of the static files should be ignored.
* @since 5.3
*/
public void setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
}
/**
@@ -376,18 +385,35 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
}
/**
* Set whether we should look at the {@link Resource#lastModified()}
* when serving resources and use this information to drive {@code "Last-Modified"}
* HTTP response headers.
* <p>This option is enabled by default and should be turned off if the metadata of
* the static files should be ignored.
* @param useLastModified whether to use the resource last-modified information.
* @since 5.3
* Set whether to optimize the specified locations through an existence
* check on startup, filtering non-existing directories upfront so that
* they do not have to be checked on every resource access.
* <p>The default is {@code false}, for defensiveness against zip files
* without directory entries which are unable to expose the existence of
* a directory upfront. Switch this flag to {@code true} for optimized
* access in case of a consistent jar layout with directory entries.
* @since 5.3.13
*/
public void setUseLastModified(boolean useLastModified) {
this.useLastModified = useLastModified;
public void setOptimizeLocations(boolean optimizeLocations) {
this.optimizeLocations = optimizeLocations;
}
/**
* Return whether to optimize the specified locations through an existence
* check on startup, filtering non-existing directories upfront so that
* they do not have to be checked on every resource access.
* @since 5.3.13
*/
public boolean isOptimizeLocations() {
return this.optimizeLocations;
}
@Override
public void setEmbeddedValueResolver(StringValueResolver resolver) {
this.embeddedValueResolver = resolver;
}
@Override
public void afterPropertiesSet() throws Exception {
resolveResourceLocations();
@@ -449,8 +475,8 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
if (location.equals("/") && !(resource instanceof ServletContextResource)) {
throw new IllegalStateException(
"The String-based location \"/\" should be relative to the web application root " +
"but resolved to a Resource of type: " + resource.getClass() + ". " +
"If this is intentional, please pass it as a pre-configured Resource via setLocations.");
"but resolved to a Resource of type: " + resource.getClass() + ". " +
"If this is intentional, please pass it as a pre-configured Resource via setLocations.");
}
result.add(resource);
if (charset != null) {
@@ -463,7 +489,9 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
}
result.addAll(this.locationResources);
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
if (isOptimizeLocations()) {
result = result.stream().filter(Resource::exists).collect(Collectors.toList());
}
this.locationsToUse.clear();
this.locationsToUse.addAll(result);
@@ -508,6 +536,7 @@ public class ResourceHttpRequestHandler extends WebContentGenerator
return null;
}
/**
* Processes a resource request.
* <p>Checks for the existence of the requested resource in the configured list of locations.
@@ -311,7 +311,7 @@ public class ResourceHttpRequestHandlerTests {
assertThat(this.response.getContentAsString()).isEqualTo("h1 { color:red; }");
}
@Test // gh-27538
@Test // gh-27538, gh-27624
public void filterNonExistingLocations() throws Exception {
List<Resource> inputLocations = Arrays.asList(
new ClassPathResource("test/", getClass()),
@@ -321,6 +321,7 @@ public class ResourceHttpRequestHandlerTests {
ResourceHttpRequestHandler handler = new ResourceHttpRequestHandler();
handler.setServletContext(new MockServletContext());
handler.setLocations(inputLocations);
handler.setOptimizeLocations(true);
handler.afterPropertiesSet();
List<Resource> actual = handler.getLocations();
+6 -4
View File
@@ -3039,7 +3039,7 @@ constructor or setter argument or autowired field) as `ObjectFactory<MyTargetBea
allowing for a `getObject()` call to retrieve the current instance on demand every
time it is needed -- without holding on to the instance or storing it separately.
As an extended variant, you may declare `ObjectProvider<MyTargetBean>`, which delivers
As an extended variant, you may declare `ObjectProvider<MyTargetBean>` which delivers
several additional access variants, including `getIfAvailable` and `getIfUnique`.
The JSR-330 variant of this is called `Provider` and is used with a `Provider<MyTargetBean>`
@@ -6675,9 +6675,11 @@ factory method and other bean definition properties, such as a qualifier value t
the `@Qualifier` annotation. Other method-level annotations that can be specified are
`@Scope`, `@Lazy`, and custom qualifier annotations.
TIP: In addition to its role for component initialization, you can also place the `@Lazy` annotation
on injection points marked with `@Autowired` or `@Inject`. In this context, it
leads to the injection of a lazy-resolution proxy.
TIP: In addition to its role for component initialization, you can also place the `@Lazy`
annotation on injection points marked with `@Autowired` or `@Inject`. In this context,
it leads to the injection of a lazy-resolution proxy. However, such a proxy approach
is rather limited. For sophisticated lazy interactions, in particular in combination
with optional dependencies, we recommend `ObjectProvider<MyTargetBean>` instead.
Autowired fields and methods are supported, as previously discussed, with additional
support for autowiring of `@Bean` methods. The following example shows how to do so:
+1 -1
View File
@@ -89,7 +89,7 @@ modified copy as follows:
=== MaxInMemorySize
Codecs have <<web-reactive.adoc#webflux-codecs-limits,limits>> for buffering data in
memory to avoid application memory issues. By the default those are set to 256KB.
memory to avoid application memory issues. By default those are set to 256KB.
If that's not enough you'll get the following error:
----
+5 -2
View File
@@ -4170,8 +4170,8 @@ the example:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(365, TimeUnit.DAYS));
}
}
@@ -4259,6 +4259,9 @@ re-write URLs to include the version of the jar and can also match against incom
without versions -- for example, from `/jquery/jquery.min.js` to
`/jquery/1.2.0/jquery.min.js`.
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
for fine-grained control, e.g. last-modified behavior and optimized resource resolution.
[[webflux-config-path-matching]]
+5 -2
View File
@@ -5738,8 +5738,8 @@ The following listing shows how to do so with Java configuration:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
.addResourceLocations("/public", "classpath:/static/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)));
}
}
----
@@ -5846,6 +5846,9 @@ re-write URLs to include the version of the jar and can also match against incom
without versions -- for example, from `/jquery/jquery.min.js` to
`/jquery/1.2.0/jquery.min.js`.
TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options
for fine-grained control, e.g. last-modified behavior and optimized resource resolution.
[[mvc-default-servlet-handler]]