Validate schema and data resources

Previously, if a user specifies a path to a schema or data DDL that does
not exist, the application will start up fine and the missing DDL would
not be reported.

This commit validates that user-defined resources actually exist and
throw a new `ResourceNotFoundException` if they don't.

Closes gh-7088
This commit is contained in:
Stephane Nicoll 2016-12-01 11:52:52 +01:00
parent 3ac22e7cdf
commit 2c630b5c61
7 changed files with 170 additions and 36 deletions

View File

@ -17,7 +17,7 @@
package org.springframework.boot.autoconfigure.jdbc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import javax.annotation.PostConstruct;
@ -27,6 +27,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.config.ResourceNotFoundException;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.Resource;
@ -43,6 +44,7 @@ import org.springframework.util.StringUtils;
* @author Dave Syer
* @author Phillip Webb
* @author Eddú Meléndez
* @author Stephane Nicoll
* @since 1.1.0
* @see DataSourceAutoConfiguration
*/
@ -78,7 +80,8 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized
}
private void runSchemaScripts() {
List<Resource> scripts = getScripts(this.properties.getSchema(), "schema");
List<Resource> scripts = getScripts("spring.datasource.schema",
this.properties.getSchema(), "schema");
if (!scripts.isEmpty()) {
String username = this.properties.getSchemaUsername();
String password = this.properties.getSchemaPassword();
@ -114,41 +117,50 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized
}
private void runDataScripts() {
List<Resource> scripts = getScripts(this.properties.getData(), "data");
List<Resource> scripts = getScripts("spring.datasource.data",
this.properties.getData(), "data");
String username = this.properties.getDataUsername();
String password = this.properties.getDataPassword();
runScripts(scripts, username, password);
}
private List<Resource> getScripts(String locations, String fallback) {
if (locations == null) {
String platform = this.properties.getPlatform();
locations = "classpath*:" + fallback + "-" + platform + ".sql,";
locations += "classpath*:" + fallback + ".sql";
private List<Resource> getScripts(String propertyName,
List<String> resources, String fallback) {
if (resources != null) {
return getResources(propertyName, resources, true);
}
return getResources(locations);
String platform = this.properties.getPlatform();
List<String> fallbackResources = new ArrayList<String>();
fallbackResources.add("classpath*:" + fallback + "-" + platform + ".sql");
fallbackResources.add("classpath*:" + fallback + ".sql");
return getResources(propertyName, fallbackResources, false);
}
private List<Resource> getResources(String locations) {
return getResources(
Arrays.asList(StringUtils.commaDelimitedListToStringArray(locations)));
}
private List<Resource> getResources(List<String> locations) {
SortedResourcesFactoryBean factory = new SortedResourcesFactoryBean(
this.applicationContext, locations);
try {
factory.afterPropertiesSet();
List<Resource> resources = new ArrayList<Resource>();
for (Resource resource : factory.getObject()) {
private List<Resource> getResources(String propertyName,
List<String> locations, boolean validate) {
List<Resource> resources = new ArrayList<Resource>();
for (String location : locations) {
for (Resource resource : doGetResources(location)) {
if (resource.exists()) {
resources.add(resource);
}
else if (validate) {
throw new ResourceNotFoundException(propertyName, resource);
}
}
return resources;
}
return resources;
}
private Resource[] doGetResources(String location) {
try {
SortedResourcesFactoryBean factory = new SortedResourcesFactoryBean(
this.applicationContext, Collections.singletonList(location));
factory.afterPropertiesSet();
return factory.getObject();
}
catch (Exception ex) {
throw new IllegalStateException("Unable to load resources from " + locations,
throw new IllegalStateException("Unable to load resources from " + location,
ex);
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.jdbc;
import java.nio.charset.Charset;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@ -106,9 +107,9 @@ public class DataSourceProperties
private String platform = "all";
/**
* Schema (DDL) script resource reference.
* Schema (DDL) script resource references.
*/
private String schema;
private List<String> schema;
/**
* User of the database to execute DDL scripts (if different).
@ -121,9 +122,9 @@ public class DataSourceProperties
private String schemaPassword;
/**
* Data (DML) script resource reference.
* Data (DML) script resource references.
*/
private String data;
private List<String> data;
/**
* User of the database to execute DML scripts.
@ -388,11 +389,11 @@ public class DataSourceProperties
this.platform = platform;
}
public String getSchema() {
public List<String> getSchema() {
return this.schema;
}
public void setSchema(String schema) {
public void setSchema(List<String> schema) {
this.schema = schema;
}
@ -412,12 +413,12 @@ public class DataSourceProperties
this.schemaPassword = schemaPassword;
}
public String getData() {
public List<String> getData() {
return this.data;
}
public void setData(String script) {
this.data = script;
public void setData(List<String> data) {
this.data = data;
}
public String getDataUsername() {

View File

@ -368,6 +368,17 @@
}
]
},
{
"name": "spring.datasource.data",
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "java.util.List<org.springframework.core.io.Resource>"
}
}
]
},
{
"name": "spring.datasource.driver-class-name",
"providers": [
@ -379,6 +390,17 @@
}
]
},
{
"name": "spring.datasource.schema",
"providers": [
{
"name": "handle-as",
"parameters": {
"target": "java.util.List<org.springframework.core.io.Resource>"
}
}
]
},
{
"name": "spring.datasource.xa.data-source-class-name",
"providers": [

View File

@ -26,7 +26,9 @@ import javax.sql.DataSource;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
@ -54,9 +56,13 @@ import static org.junit.Assert.fail;
* Tests for {@link DataSourceInitializer}.
*
* @author Dave Syer
* @author Stephane Nicoll
*/
public class DataSourceInitializerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@Before
@ -271,6 +277,37 @@ public class DataSourceInitializerTests {
.isEqualTo(1);
}
@Test
public void testDataSourceInitializedWithInvalidSchemaResource() {
this.context.register(DataSourceAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.datasource.initialize:true",
"spring.datasource.schema:classpath:does/not/exist.sql");
this.thrown.expect(BeanCreationException.class);
this.thrown.expectMessage("does/not/exist.sql");
this.thrown.expectMessage("spring.datasource.schema");
this.context.refresh();
}
@Test
public void testDataSourceInitializedWithInvalidDataResource() {
this.context.register(DataSourceAutoConfiguration.class,
PropertyPlaceholderAutoConfiguration.class);
EnvironmentTestUtils.addEnvironment(this.context,
"spring.datasource.initialize:true",
"spring.datasource.schema:"
+ ClassUtils.addResourcePathToPackagePath(getClass(),
"schema.sql"),
"spring.datasource.data:classpath:does/not/exist.sql");
this.thrown.expect(BeanCreationException.class);
this.thrown.expectMessage("does/not/exist.sql");
this.thrown.expectMessage("spring.datasource.data");
this.context.refresh();
}
@Configuration
@EnableConfigurationProperties
protected static class TwoDataSources {

View File

@ -27,7 +27,9 @@ import javax.transaction.UserTransaction;
import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform;
import org.junit.After;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
@ -50,6 +52,9 @@ import static org.assertj.core.api.Assertions.assertThat;
public class HibernateJpaAutoConfigurationTests
extends AbstractJpaAutoConfigurationTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@After
public void cleanup() {
HibernateVersion.setRunning(null);
@ -67,9 +72,10 @@ public class HibernateJpaAutoConfigurationTests
// Missing:
"spring.datasource.schema:classpath:/ddl.sql");
setupTestConfiguration();
this.thrown.expectMessage("ddl.sql");
this.thrown.expectMessage("spring.datasource.schema");
this.context.refresh();
assertThat(new JdbcTemplate(this.context.getBean(DataSource.class))
.queryForObject("SELECT COUNT(*) from CITY", Integer.class)).isEqualTo(1);
}
// This can't succeed because the data SQL is executed immediately after the schema

View File

@ -603,7 +603,7 @@ content into your application; rather pick only the properties that you need.
# DATASOURCE ({sc-spring-boot-autoconfigure}/jdbc/DataSourceAutoConfiguration.{sc-ext}[DataSourceAutoConfiguration] & {sc-spring-boot-autoconfigure}/jdbc/DataSourceProperties.{sc-ext}[DataSourceProperties])
spring.datasource.continue-on-error=false # Do not stop if an error occurs while initializing the database.
spring.datasource.data= # Data (DML) script resource reference.
spring.datasource.data= # Data (DML) script resource references.
spring.datasource.data-username= # User of the database to execute DML scripts (if different).
spring.datasource.data-password= # Password of the database to execute DML scripts (if different).
spring.datasource.dbcp2.*= # Commons DBCP2 specific settings
@ -616,7 +616,7 @@ content into your application; rather pick only the properties that you need.
spring.datasource.name=testdb # Name of the datasource.
spring.datasource.password= # Login password of the database.
spring.datasource.platform=all # Platform to use in the schema resource (schema-${platform}.sql).
spring.datasource.schema= # Schema (DDL) script resource reference.
spring.datasource.schema= # Schema (DDL) script resource references.
spring.datasource.schema-username= # User of the database to execute DDL scripts (if different).
spring.datasource.schema-password= # Password of the database to execute DDL scripts (if different).
spring.datasource.separator=; # Statement separator in SQL initialization scripts.

View File

@ -0,0 +1,56 @@
/*
* Copyright 2012-2016 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
*
* http://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.boot.context.config;
import org.springframework.core.io.Resource;
/**
* Exception thrown when a {@link Resource} defined by a property is not found.
*
* @author Stephane Nicoll
* @since 1.5.0
*/
@SuppressWarnings("serial")
public class ResourceNotFoundException extends RuntimeException {
private final String propertyName;
private final Resource resource;
public ResourceNotFoundException(String propertyName, Resource resource) {
super(String.format("%s defined by '%s' does not exist", resource, propertyName));
this.propertyName = propertyName;
this.resource = resource;
}
/**
* Return the name of the property that defines the resource.
* @return the property
*/
public String getPropertyName() {
return this.propertyName;
}
/**
* Return the {@link Resource}.
* @return the resource
*/
public Resource getResource() {
return this.resource;
}
}