Commit 2c630b5c authored by Stephane Nicoll's avatar Stephane Nicoll

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
parent 3ac22e7c
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
package org.springframework.boot.autoconfigure.jdbc; package org.springframework.boot.autoconfigure.jdbc;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Collections;
import java.util.List; import java.util.List;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
...@@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; ...@@ -27,6 +27,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.config.ResourceNotFoundException;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
...@@ -43,6 +44,7 @@ import org.springframework.util.StringUtils; ...@@ -43,6 +44,7 @@ import org.springframework.util.StringUtils;
* @author Dave Syer * @author Dave Syer
* @author Phillip Webb * @author Phillip Webb
* @author Eddú Meléndez * @author Eddú Meléndez
* @author Stephane Nicoll
* @since 1.1.0 * @since 1.1.0
* @see DataSourceAutoConfiguration * @see DataSourceAutoConfiguration
*/ */
...@@ -78,7 +80,8 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized ...@@ -78,7 +80,8 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized
} }
private void runSchemaScripts() { 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()) { if (!scripts.isEmpty()) {
String username = this.properties.getSchemaUsername(); String username = this.properties.getSchemaUsername();
String password = this.properties.getSchemaPassword(); String password = this.properties.getSchemaPassword();
...@@ -114,41 +117,50 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized ...@@ -114,41 +117,50 @@ class DataSourceInitializer implements ApplicationListener<DataSourceInitialized
} }
private void runDataScripts() { 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 username = this.properties.getDataUsername();
String password = this.properties.getDataPassword(); String password = this.properties.getDataPassword();
runScripts(scripts, username, password); runScripts(scripts, username, password);
} }
private List<Resource> getScripts(String locations, String fallback) { private List<Resource> getScripts(String propertyName,
if (locations == null) { List<String> resources, String fallback) {
String platform = this.properties.getPlatform(); if (resources != null) {
locations = "classpath*:" + fallback + "-" + platform + ".sql,"; return getResources(propertyName, resources, true);
locations += "classpath*:" + fallback + ".sql";
} }
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) { private List<Resource> getResources(String propertyName,
return getResources( List<String> locations, boolean validate) {
Arrays.asList(StringUtils.commaDelimitedListToStringArray(locations))); List<Resource> resources = new ArrayList<Resource>();
} for (String location : locations) {
for (Resource resource : doGetResources(location)) {
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()) {
if (resource.exists()) { if (resource.exists()) {
resources.add(resource); 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) { catch (Exception ex) {
throw new IllegalStateException("Unable to load resources from " + locations, throw new IllegalStateException("Unable to load resources from " + location,
ex); ex);
} }
} }
......
...@@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.jdbc; ...@@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.jdbc;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
...@@ -106,9 +107,9 @@ public class DataSourceProperties ...@@ -106,9 +107,9 @@ public class DataSourceProperties
private String platform = "all"; 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). * User of the database to execute DDL scripts (if different).
...@@ -121,9 +122,9 @@ public class DataSourceProperties ...@@ -121,9 +122,9 @@ public class DataSourceProperties
private String schemaPassword; 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. * User of the database to execute DML scripts.
...@@ -388,11 +389,11 @@ public class DataSourceProperties ...@@ -388,11 +389,11 @@ public class DataSourceProperties
this.platform = platform; this.platform = platform;
} }
public String getSchema() { public List<String> getSchema() {
return this.schema; return this.schema;
} }
public void setSchema(String schema) { public void setSchema(List<String> schema) {
this.schema = schema; this.schema = schema;
} }
...@@ -412,12 +413,12 @@ public class DataSourceProperties ...@@ -412,12 +413,12 @@ public class DataSourceProperties
this.schemaPassword = schemaPassword; this.schemaPassword = schemaPassword;
} }
public String getData() { public List<String> getData() {
return this.data; return this.data;
} }
public void setData(String script) { public void setData(List<String> data) {
this.data = script; this.data = data;
} }
public String getDataUsername() { public String getDataUsername() {
......
...@@ -368,6 +368,17 @@ ...@@ -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", "name": "spring.datasource.driver-class-name",
"providers": [ "providers": [
...@@ -379,6 +390,17 @@ ...@@ -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", "name": "spring.datasource.xa.data-source-class-name",
"providers": [ "providers": [
......
...@@ -26,7 +26,9 @@ import javax.sql.DataSource; ...@@ -26,7 +26,9 @@ import javax.sql.DataSource;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration; import org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration;
...@@ -54,9 +56,13 @@ import static org.junit.Assert.fail; ...@@ -54,9 +56,13 @@ import static org.junit.Assert.fail;
* Tests for {@link DataSourceInitializer}. * Tests for {@link DataSourceInitializer}.
* *
* @author Dave Syer * @author Dave Syer
* @author Stephane Nicoll
*/ */
public class DataSourceInitializerTests { public class DataSourceInitializerTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); private final AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
@Before @Before
...@@ -271,6 +277,37 @@ public class DataSourceInitializerTests { ...@@ -271,6 +277,37 @@ public class DataSourceInitializerTests {
.isEqualTo(1); .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 @Configuration
@EnableConfigurationProperties @EnableConfigurationProperties
protected static class TwoDataSources { protected static class TwoDataSources {
......
...@@ -27,7 +27,9 @@ import javax.transaction.UserTransaction; ...@@ -27,7 +27,9 @@ import javax.transaction.UserTransaction;
import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform;
import org.junit.After; import org.junit.After;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration;
...@@ -50,6 +52,9 @@ import static org.assertj.core.api.Assertions.assertThat; ...@@ -50,6 +52,9 @@ import static org.assertj.core.api.Assertions.assertThat;
public class HibernateJpaAutoConfigurationTests public class HibernateJpaAutoConfigurationTests
extends AbstractJpaAutoConfigurationTests { extends AbstractJpaAutoConfigurationTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
@After @After
public void cleanup() { public void cleanup() {
HibernateVersion.setRunning(null); HibernateVersion.setRunning(null);
...@@ -67,9 +72,10 @@ public class HibernateJpaAutoConfigurationTests ...@@ -67,9 +72,10 @@ public class HibernateJpaAutoConfigurationTests
// Missing: // Missing:
"spring.datasource.schema:classpath:/ddl.sql"); "spring.datasource.schema:classpath:/ddl.sql");
setupTestConfiguration(); setupTestConfiguration();
this.thrown.expectMessage("ddl.sql");
this.thrown.expectMessage("spring.datasource.schema");
this.context.refresh(); 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 // This can't succeed because the data SQL is executed immediately after the schema
......
...@@ -603,7 +603,7 @@ content into your application; rather pick only the properties that you need. ...@@ -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]) # 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.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-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.data-password= # Password of the database to execute DML scripts (if different).
spring.datasource.dbcp2.*= # Commons DBCP2 specific settings spring.datasource.dbcp2.*= # Commons DBCP2 specific settings
...@@ -616,7 +616,7 @@ content into your application; rather pick only the properties that you need. ...@@ -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.name=testdb # Name of the datasource.
spring.datasource.password= # Login password of the database. spring.datasource.password= # Login password of the database.
spring.datasource.platform=all # Platform to use in the schema resource (schema-${platform}.sql). 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-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.schema-password= # Password of the database to execute DDL scripts (if different).
spring.datasource.separator=; # Statement separator in SQL initialization scripts. spring.datasource.separator=; # Statement separator in SQL initialization scripts.
......
/*
* 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;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment