Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in / Register
Toggle navigation
S
spring-boot
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
DEMO
spring-boot
Commits
5dee68c9
Commit
5dee68c9
authored
Mar 22, 2021
by
Andy Wilkinson
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Move script-based DataSource initializer into spring-boot
Closes gh-25487 Closes gh-25756
parent
fa336bb5
Changes
16
Hide whitespace changes
Inline
Side-by-side
Showing
16 changed files
with
579 additions
and
318 deletions
+579
-318
DataSourceAutoConfiguration.java
.../boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java
+3
-1
DataSourceInitializationConfiguration.java
...configure/jdbc/DataSourceInitializationConfiguration.java
+138
-7
DataSourceInitializer.java
...mework/boot/autoconfigure/jdbc/DataSourceInitializer.java
+0
-196
spring.factories
...utoconfigure/src/main/resources/META-INF/spring.factories
+1
-2
DataSourceInitializationIntegrationTests.java
...figure/jdbc/DataSourceInitializationIntegrationTests.java
+4
-8
DataSourceInitializerTests.java
...k/boot/autoconfigure/jdbc/DataSourceInitializerTests.java
+0
-99
HibernateJpaAutoConfigurationTests.java
...configure/orm/jpa/HibernateJpaAutoConfigurationTests.java
+0
-1
howto.adoc
...oot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc
+4
-0
DataSourceInitializationSettings.java
...work/boot/jdbc/init/DataSourceInitializationSettings.java
+128
-0
ScriptDataSourceInitializer.java
...framework/boot/jdbc/init/ScriptDataSourceInitializer.java
+188
-0
ScriptDataSourceInitializerDetector.java
...k/boot/jdbc/init/ScriptDataSourceInitializerDetector.java
+4
-4
package-info.java
...java/org/springframework/boot/jdbc/init/package-info.java
+20
-0
spring.factories
.../spring-boot/src/main/resources/META-INF/spring.factories
+1
-0
ScriptDataSourceInitializerTests.java
...work/boot/jdbc/init/ScriptDataSourceInitializerTests.java
+83
-0
data.sql
spring-boot-project/spring-boot/src/test/resources/data.sql
+1
-0
schema.sql
...ng-boot-project/spring-boot/src/test/resources/schema.sql
+4
-0
No files found.
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceAutoConfiguration.java
View file @
5dee68c9
...
...
@@ -54,7 +54,9 @@ import org.springframework.util.StringUtils;
@ConditionalOnClass
({
DataSource
.
class
,
EmbeddedDatabaseType
.
class
})
@ConditionalOnMissingBean
(
type
=
"io.r2dbc.spi.ConnectionFactory"
)
@EnableConfigurationProperties
(
DataSourceProperties
.
class
)
@Import
({
DataSourcePoolMetadataProvidersConfiguration
.
class
,
DataSourceInitializationConfiguration
.
class
})
@Import
({
DataSourcePoolMetadataProvidersConfiguration
.
class
,
DataSourceInitializationConfiguration
.
InitializationSpecificCredentialsDataSourceInitializationConfiguration
.
class
,
DataSourceInitializationConfiguration
.
SharedCredentialsDataSourceInitializationConfiguration
.
class
})
public
class
DataSourceAutoConfiguration
{
@Configuration
(
proxyBeanMethods
=
false
)
...
...
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationConfiguration.java
View file @
5dee68c9
...
...
@@ -16,27 +16,158 @@
package
org
.
springframework
.
boot
.
autoconfigure
.
jdbc
;
import
java.nio.charset.Charset
;
import
java.util.ArrayList
;
import
java.util.List
;
import
java.util.function.Supplier
;
import
javax.sql.DataSource
;
import
org.springframework.beans.factory.ObjectProvider
;
import
org.springframework.boot.autoconfigure.condition.AnyNestedCondition
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
;
import
org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate
;
import
org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.DifferentCredentialsCondition
;
import
org.springframework.boot.jdbc.DataSourceBuilder
;
import
org.springframework.boot.jdbc.DataSourceInitializationMode
;
import
org.springframework.boot.jdbc.EmbeddedDatabaseConnection
;
import
org.springframework.boot.jdbc.init.DataSourceInitializationSettings
;
import
org.springframework.boot.jdbc.init.ScriptDataSourceInitializer
;
import
org.springframework.boot.jdbc.init.dependency.DataSourceInitializationDependencyConfigurer
;
import
org.springframework.context.annotation.Bean
;
import
org.springframework.context.annotation.Configuration
;
import
org.springframework.context.annotation.DependsOn
;
import
org.springframework.context.annotation.Import
;
import
org.springframework.core.io.Resource
;
import
org.springframework.core.io.ResourceLoader
;
import
org.springframework.jdbc.datasource.SimpleDriverDataSource
;
import
org.springframework.util.StringUtils
;
/**
* Configuration for {@link DataSource} initialization using DDL and DML scripts.
* Configuration for {@link DataSource} initialization using a
* {@link ScriptDataSourceInitializer} with DDL and DML scripts.
*
* @author Andy Wilkinson
*/
@Configuration
(
proxyBeanMethods
=
false
)
@ConditionalOnSingleCandidate
(
DataSource
.
class
)
@Import
(
DataSourceInitializationDependencyConfigurer
.
class
)
class
DataSourceInitializationConfiguration
{
@Bean
DataSourceInitialization
dataSourceInitialization
(
DataSource
dataSource
,
DataSourceProperties
properties
)
{
return
new
DataSourceInitialization
(
dataSource
,
properties
);
private
static
DataSource
determineDataSource
(
Supplier
<
DataSource
>
dataSource
,
String
username
,
String
password
,
DataSourceProperties
properties
)
{
if
(
StringUtils
.
hasText
(
username
)
&&
StringUtils
.
hasText
(
password
))
{
DataSourceBuilder
.
derivedFrom
(
dataSource
.
get
()).
type
(
SimpleDriverDataSource
.
class
).
username
(
username
)
.
password
(
password
).
build
();
}
return
dataSource
.
get
();
}
private
static
List
<
String
>
scriptLocations
(
List
<
String
>
locations
,
String
fallback
,
String
platform
)
{
if
(
locations
!=
null
)
{
return
locations
;
}
List
<
String
>
fallbackLocations
=
new
ArrayList
<>();
fallbackLocations
.
add
(
"optional:classpath*:"
+
fallback
+
"-"
+
platform
+
".sql"
);
fallbackLocations
.
add
(
"optional:classpath*:"
+
fallback
+
".sql"
);
return
fallbackLocations
;
}
// Fully-qualified to work around javac bug in JDK 1.8
@org
.
springframework
.
context
.
annotation
.
Configuration
(
proxyBeanMethods
=
false
)
@org
.
springframework
.
context
.
annotation
.
Conditional
(
DifferentCredentialsCondition
.
class
)
@org
.
springframework
.
context
.
annotation
.
Import
(
DataSourceInitializationDependencyConfigurer
.
class
)
@ConditionalOnSingleCandidate
(
DataSource
.
class
)
@ConditionalOnMissingBean
(
ScriptDataSourceInitializer
.
class
)
static
class
InitializationSpecificCredentialsDataSourceInitializationConfiguration
{
@Bean
ScriptDataSourceInitializer
ddlOnlyScriptDataSourceInitializer
(
ObjectProvider
<
DataSource
>
dataSource
,
DataSourceProperties
properties
,
ResourceLoader
resourceLoader
)
{
DataSourceInitializationSettings
settings
=
new
DataSourceInitializationSettings
();
settings
.
setDdlScriptLocations
(
scriptLocations
(
properties
.
getSchema
(),
"schema"
,
properties
.
getPlatform
()));
settings
.
setContinueOnError
(
properties
.
isContinueOnError
());
settings
.
setSeparator
(
properties
.
getSeparator
());
settings
.
setEncoding
(
properties
.
getSqlScriptEncoding
());
DataSource
initializationDataSource
=
determineDataSource
(
dataSource:
:
getObject
,
properties
.
getSchemaUsername
(),
properties
.
getSchemaPassword
(),
properties
);
return
new
InitializationModeDataSourceScriptDatabaseInitializer
(
initializationDataSource
,
settings
,
properties
.
getInitializationMode
());
}
@Bean
@DependsOn
(
"ddlOnlyScriptDataSourceInitializer"
)
ScriptDataSourceInitializer
dmlOnlyScriptDataSourceInitializer
(
ObjectProvider
<
DataSource
>
dataSource
,
DataSourceProperties
properties
,
ResourceLoader
resourceLoader
)
{
DataSourceInitializationSettings
settings
=
new
DataSourceInitializationSettings
();
settings
.
setDmlScriptLocations
(
scriptLocations
(
properties
.
getData
(),
"data"
,
properties
.
getPlatform
()));
settings
.
setContinueOnError
(
properties
.
isContinueOnError
());
settings
.
setSeparator
(
properties
.
getSeparator
());
settings
.
setEncoding
(
properties
.
getSqlScriptEncoding
());
DataSource
initializationDataSource
=
determineDataSource
(
dataSource:
:
getObject
,
properties
.
getDataUsername
(),
properties
.
getDataPassword
(),
properties
);
return
new
InitializationModeDataSourceScriptDatabaseInitializer
(
initializationDataSource
,
settings
,
properties
.
getInitializationMode
());
}
static
class
DifferentCredentialsCondition
extends
AnyNestedCondition
{
DifferentCredentialsCondition
()
{
super
(
ConfigurationPhase
.
PARSE_CONFIGURATION
);
}
@ConditionalOnProperty
(
prefix
=
"spring.datasource"
,
name
=
"schema-username"
)
static
class
SchemaCredentials
{
}
@ConditionalOnProperty
(
prefix
=
"spring.datasource"
,
name
=
"data-username"
)
static
class
DataCredentials
{
}
}
}
@Configuration
(
proxyBeanMethods
=
false
)
@ConditionalOnSingleCandidate
(
DataSource
.
class
)
@ConditionalOnMissingBean
(
ScriptDataSourceInitializer
.
class
)
@Import
(
DataSourceInitializationDependencyConfigurer
.
class
)
static
class
SharedCredentialsDataSourceInitializationConfiguration
{
@Bean
ScriptDataSourceInitializer
scriptDataSourceInitializer
(
DataSource
dataSource
,
DataSourceProperties
properties
,
ResourceLoader
resourceLoader
)
{
DataSourceInitializationSettings
settings
=
new
DataSourceInitializationSettings
();
settings
.
setDdlScriptLocations
(
scriptLocations
(
properties
.
getSchema
(),
"schema"
,
properties
.
getPlatform
()));
settings
.
setDmlScriptLocations
(
scriptLocations
(
properties
.
getData
(),
"data"
,
properties
.
getPlatform
()));
settings
.
setContinueOnError
(
properties
.
isContinueOnError
());
settings
.
setSeparator
(
properties
.
getSeparator
());
settings
.
setEncoding
(
properties
.
getSqlScriptEncoding
());
return
new
InitializationModeDataSourceScriptDatabaseInitializer
(
dataSource
,
settings
,
properties
.
getInitializationMode
());
}
}
static
class
InitializationModeDataSourceScriptDatabaseInitializer
extends
ScriptDataSourceInitializer
{
private
final
DataSourceInitializationMode
mode
;
InitializationModeDataSourceScriptDatabaseInitializer
(
DataSource
dataSource
,
DataSourceInitializationSettings
settings
,
DataSourceInitializationMode
mode
)
{
super
(
dataSource
,
settings
);
this
.
mode
=
mode
;
}
@Override
protected
void
runScripts
(
List
<
Resource
>
resources
,
boolean
continueOnError
,
String
separator
,
Charset
encoding
)
{
if
(
this
.
mode
==
DataSourceInitializationMode
.
ALWAYS
||
(
this
.
mode
==
DataSourceInitializationMode
.
EMBEDDED
&&
EmbeddedDatabaseConnection
.
isEmbedded
(
getDataSource
())))
{
super
.
runScripts
(
resources
,
continueOnError
,
separator
,
encoding
);
}
}
}
}
spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializer.java
deleted
100644 → 0
View file @
fa336bb5
/*
* Copyright 2012-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
.
boot
.
autoconfigure
.
jdbc
;
import
java.util.ArrayList
;
import
java.util.Collections
;
import
java.util.List
;
import
javax.sql.DataSource
;
import
org.apache.commons.logging.Log
;
import
org.apache.commons.logging.LogFactory
;
import
org.springframework.boot.context.properties.source.InvalidConfigurationPropertyValueException
;
import
org.springframework.boot.jdbc.DataSourceBuilder
;
import
org.springframework.boot.jdbc.DataSourceInitializationMode
;
import
org.springframework.boot.jdbc.EmbeddedDatabaseConnection
;
import
org.springframework.core.io.DefaultResourceLoader
;
import
org.springframework.core.io.Resource
;
import
org.springframework.core.io.ResourceLoader
;
import
org.springframework.jdbc.config.SortedResourcesFactoryBean
;
import
org.springframework.jdbc.datasource.SimpleDriverDataSource
;
import
org.springframework.jdbc.datasource.init.DatabasePopulatorUtils
;
import
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
;
import
org.springframework.util.StringUtils
;
/**
* Initialize a {@link DataSource} based on a matching {@link DataSourceProperties}
* config.
*
* @author Dave Syer
* @author Phillip Webb
* @author Eddú Meléndez
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @since 2.5.0
*/
public
class
DataSourceInitializer
{
private
static
final
Log
logger
=
LogFactory
.
getLog
(
DataSourceInitializer
.
class
);
private
final
DataSource
dataSource
;
private
final
DataSourceProperties
properties
;
private
final
ResourceLoader
resourceLoader
;
/**
* Create a new instance with the {@link DataSource} to initialize and its matching
* {@link DataSourceProperties configuration}.
* @param dataSource the datasource to initialize
* @param properties the matching configuration
* @param resourceLoader the resource loader to use (can be null)
*/
public
DataSourceInitializer
(
DataSource
dataSource
,
DataSourceProperties
properties
,
ResourceLoader
resourceLoader
)
{
this
.
dataSource
=
dataSource
;
this
.
properties
=
properties
;
this
.
resourceLoader
=
(
resourceLoader
!=
null
)
?
resourceLoader
:
new
DefaultResourceLoader
(
null
);
}
/**
* Initializes the {@link DataSource} by running DDL and DML scripts.
* @return {@code true} if one or more scripts were applied to the database, otherwise
* {@code false}
*/
public
boolean
initializeDataSource
()
{
boolean
initialized
=
createSchema
();
initialized
=
initSchema
()
&&
initialized
;
return
initialized
;
}
private
boolean
createSchema
()
{
List
<
Resource
>
scripts
=
getScripts
(
"spring.datasource.schema"
,
this
.
properties
.
getSchema
(),
"schema"
);
if
(!
scripts
.
isEmpty
())
{
if
(!
isEnabled
())
{
logger
.
debug
(
"Initialization disabled (not running DDL scripts)"
);
return
false
;
}
String
username
=
this
.
properties
.
getSchemaUsername
();
String
password
=
this
.
properties
.
getSchemaPassword
();
runScripts
(
scripts
,
username
,
password
);
}
return
!
scripts
.
isEmpty
();
}
private
boolean
initSchema
()
{
List
<
Resource
>
scripts
=
getScripts
(
"spring.datasource.data"
,
this
.
properties
.
getData
(),
"data"
);
if
(!
scripts
.
isEmpty
())
{
if
(!
isEnabled
())
{
logger
.
debug
(
"Initialization disabled (not running data scripts)"
);
return
false
;
}
String
username
=
this
.
properties
.
getDataUsername
();
String
password
=
this
.
properties
.
getDataPassword
();
runScripts
(
scripts
,
username
,
password
);
}
return
!
scripts
.
isEmpty
();
}
private
boolean
isEnabled
()
{
DataSourceInitializationMode
mode
=
this
.
properties
.
getInitializationMode
();
if
(
mode
==
DataSourceInitializationMode
.
NEVER
)
{
return
false
;
}
if
(
mode
==
DataSourceInitializationMode
.
EMBEDDED
&&
!
isEmbedded
())
{
return
false
;
}
return
true
;
}
private
boolean
isEmbedded
()
{
try
{
return
EmbeddedDatabaseConnection
.
isEmbedded
(
this
.
dataSource
);
}
catch
(
Exception
ex
)
{
logger
.
debug
(
"Could not determine if datasource is embedded"
,
ex
);
return
false
;
}
}
private
List
<
Resource
>
getScripts
(
String
propertyName
,
List
<
String
>
resources
,
String
fallback
)
{
if
(
resources
!=
null
)
{
return
getResources
(
propertyName
,
resources
,
true
);
}
String
platform
=
this
.
properties
.
getPlatform
();
List
<
String
>
fallbackResources
=
new
ArrayList
<>();
fallbackResources
.
add
(
"classpath*:"
+
fallback
+
"-"
+
platform
+
".sql"
);
fallbackResources
.
add
(
"classpath*:"
+
fallback
+
".sql"
);
return
getResources
(
propertyName
,
fallbackResources
,
false
);
}
private
List
<
Resource
>
getResources
(
String
propertyName
,
List
<
String
>
locations
,
boolean
validate
)
{
List
<
Resource
>
resources
=
new
ArrayList
<>();
for
(
String
location
:
locations
)
{
for
(
Resource
resource
:
doGetResources
(
location
))
{
if
(
resource
.
exists
())
{
resources
.
add
(
resource
);
}
else
if
(
validate
)
{
throw
new
InvalidConfigurationPropertyValueException
(
propertyName
,
resource
,
"No resources were found at location '"
+
location
+
"'."
);
}
}
}
return
resources
;
}
private
Resource
[]
doGetResources
(
String
location
)
{
try
{
SortedResourcesFactoryBean
factory
=
new
SortedResourcesFactoryBean
(
this
.
resourceLoader
,
Collections
.
singletonList
(
location
));
factory
.
afterPropertiesSet
();
return
factory
.
getObject
();
}
catch
(
Exception
ex
)
{
throw
new
IllegalStateException
(
"Unable to load resources from "
+
location
,
ex
);
}
}
private
void
runScripts
(
List
<
Resource
>
resources
,
String
username
,
String
password
)
{
if
(
resources
.
isEmpty
())
{
return
;
}
ResourceDatabasePopulator
populator
=
new
ResourceDatabasePopulator
();
populator
.
setContinueOnError
(
this
.
properties
.
isContinueOnError
());
populator
.
setSeparator
(
this
.
properties
.
getSeparator
());
if
(
this
.
properties
.
getSqlScriptEncoding
()
!=
null
)
{
populator
.
setSqlScriptEncoding
(
this
.
properties
.
getSqlScriptEncoding
().
name
());
}
for
(
Resource
resource
:
resources
)
{
populator
.
addScript
(
resource
);
}
DataSource
dataSource
=
this
.
dataSource
;
if
(
StringUtils
.
hasText
(
username
)
&&
dataSource
!=
null
)
{
dataSource
=
DataSourceBuilder
.
derivedFrom
(
dataSource
).
type
(
SimpleDriverDataSource
.
class
).
username
(
username
)
.
password
(
password
).
build
();
}
DatabasePopulatorUtils
.
execute
(
populator
,
dataSource
);
}
}
spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories
View file @
5dee68c9
...
...
@@ -169,5 +169,4 @@ org.springframework.boot.autoconfigure.web.servlet.JspTemplateAvailabilityProvid
# DataSource initializer detectors
org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector=\
org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDataSourceInitializerDetector,\
org.springframework.boot.autoconfigure.jdbc.DataSourceInitializationDataSourceInitializerDetector
org.springframework.boot.autoconfigure.flyway.FlywayMigrationInitializerDataSourceInitializerDetector
\ No newline at end of file
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializationIntegrationTests.java
View file @
5dee68c9
...
...
@@ -242,10 +242,8 @@ class DataSourceInitializationIntegrationTests {
"spring.datasource.schema:classpath:does/not/exist.sql"
).
run
((
context
)
->
{
assertThat
(
context
).
hasFailed
();
assertThat
(
context
.
getStartupFailure
()).
isInstanceOf
(
BeanCreationException
.
class
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"[does/not/exist.sql]"
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"spring.datasource.schema"
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"No resources were found at location 'classpath:does/not/exist.sql'."
);
assertThat
(
context
.
getStartupFailure
())
.
hasMessageContaining
(
"No DDL scripts found at location 'classpath:does/not/exist.sql'"
);
});
}
...
...
@@ -256,10 +254,8 @@ class DataSourceInitializationIntegrationTests {
"spring.datasource.data:classpath:does/not/exist.sql"
).
run
((
context
)
->
{
assertThat
(
context
).
hasFailed
();
assertThat
(
context
.
getStartupFailure
()).
isInstanceOf
(
BeanCreationException
.
class
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"[does/not/exist.sql]"
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"spring.datasource.data"
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"No resources were found at location 'classpath:does/not/exist.sql'."
);
assertThat
(
context
.
getStartupFailure
())
.
hasMessageContaining
(
"No DML scripts found at location 'classpath:does/not/exist.sql'"
);
});
}
...
...
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitializerTests.java
deleted
100644 → 0
View file @
fa336bb5
/*
* Copyright 2012-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
.
boot
.
autoconfigure
.
jdbc
;
import
java.sql.Connection
;
import
java.sql.DatabaseMetaData
;
import
java.sql.SQLException
;
import
java.util.UUID
;
import
javax.sql.DataSource
;
import
com.zaxxer.hikari.HikariDataSource
;
import
org.junit.jupiter.api.Test
;
import
org.springframework.boot.jdbc.DataSourceBuilder
;
import
org.springframework.boot.jdbc.DataSourceInitializationMode
;
import
org.springframework.jdbc.core.JdbcTemplate
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThat
;
import
static
org
.
mockito
.
BDDMockito
.
given
;
import
static
org
.
mockito
.
Mockito
.
mock
;
import
static
org
.
mockito
.
Mockito
.
times
;
import
static
org
.
mockito
.
Mockito
.
verify
;
/**
* Tests for {@link DataSourceInitializer}.
*
* @author Stephane Nicoll
*/
class
DataSourceInitializerTests
{
@Test
void
initializeEmbeddedByDefault
()
{
try
(
HikariDataSource
dataSource
=
createDataSource
())
{
DataSourceInitializer
initializer
=
new
DataSourceInitializer
(
dataSource
,
new
DataSourceProperties
(),
null
);
JdbcTemplate
jdbcTemplate
=
new
JdbcTemplate
(
dataSource
);
initializer
.
initializeDataSource
();
assertNumberOfRows
(
jdbcTemplate
,
1
);
}
}
@Test
void
initializeWithModeAlways
()
{
try
(
HikariDataSource
dataSource
=
createDataSource
())
{
DataSourceProperties
properties
=
new
DataSourceProperties
();
properties
.
setInitializationMode
(
DataSourceInitializationMode
.
ALWAYS
);
DataSourceInitializer
initializer
=
new
DataSourceInitializer
(
dataSource
,
properties
,
null
);
JdbcTemplate
jdbcTemplate
=
new
JdbcTemplate
(
dataSource
);
initializer
.
initializeDataSource
();
assertNumberOfRows
(
jdbcTemplate
,
1
);
}
}
private
void
assertNumberOfRows
(
JdbcTemplate
jdbcTemplate
,
int
count
)
{
assertThat
(
jdbcTemplate
.
queryForObject
(
"SELECT COUNT(*) from BAR"
,
Integer
.
class
)).
isEqualTo
(
count
);
}
@Test
void
initializeWithModeNever
()
{
try
(
HikariDataSource
dataSource
=
createDataSource
())
{
DataSourceProperties
properties
=
new
DataSourceProperties
();
properties
.
setInitializationMode
(
DataSourceInitializationMode
.
NEVER
);
DataSourceInitializer
initializer
=
new
DataSourceInitializer
(
dataSource
,
properties
,
null
);
assertThat
(
initializer
.
initializeDataSource
()).
isFalse
();
}
}
@Test
void
initializeOnlyEmbeddedByDefault
()
throws
SQLException
{
DatabaseMetaData
metadata
=
mock
(
DatabaseMetaData
.
class
);
given
(
metadata
.
getDatabaseProductName
()).
willReturn
(
"MySQL"
);
Connection
connection
=
mock
(
Connection
.
class
);
given
(
connection
.
getMetaData
()).
willReturn
(
metadata
);
DataSource
dataSource
=
mock
(
DataSource
.
class
);
given
(
dataSource
.
getConnection
()).
willReturn
(
connection
);
DataSourceInitializer
initializer
=
new
DataSourceInitializer
(
dataSource
,
new
DataSourceProperties
(),
null
);
assertThat
(
initializer
.
initializeDataSource
()).
isFalse
();
verify
(
dataSource
,
times
(
2
)).
getConnection
();
}
private
HikariDataSource
createDataSource
()
{
return
DataSourceBuilder
.
create
().
type
(
HikariDataSource
.
class
).
url
(
"jdbc:h2:mem:"
+
UUID
.
randomUUID
()).
build
();
}
}
spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/orm/jpa/HibernateJpaAutoConfigurationTests.java
View file @
5dee68c9
...
...
@@ -104,7 +104,6 @@ class HibernateJpaAutoConfigurationTests extends AbstractJpaAutoConfigurationTes
"spring.datasource.schema:classpath:/ddl.sql"
).
run
((
context
)
->
{
assertThat
(
context
).
hasFailed
();
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"ddl.sql"
);
assertThat
(
context
.
getStartupFailure
()).
hasMessageContaining
(
"spring.datasource.schema"
);
});
}
...
...
spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto.adoc
View file @
5dee68c9
...
...
@@ -2056,6 +2056,10 @@ By default, Spring Boot enables the fail-fast feature of the Spring JDBC initial
This means that, if the scripts cause exceptions, the application fails to start.
You can tune that behavior by setting `spring.datasource.continue-on-error`.
To take complete control over the script-based initialization of a `DataSource`, define your own `ScriptDataSourceInitializer` bean.
Doing so will cause the auto-configuration of script-based initialization to back off.
If you have multiple `DataSource`s in your application, you can define multiple `ScriptDataSourceInitializer` beans.
[[howto-initialize-a-database-using-r2dbc]]
...
...
spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/DataSourceInitializationSettings.java
0 → 100644
View file @
5dee68c9
/*
* Copyright 2012-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
.
boot
.
jdbc
.
init
;
import
java.nio.charset.Charset
;
import
java.util.List
;
import
javax.sql.DataSource
;
/**
* Settings for initializing a database using a JDBC {@link DataSource}.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public
class
DataSourceInitializationSettings
{
private
List
<
String
>
ddlScriptLocations
;
private
List
<
String
>
dmlScriptLocations
;
private
boolean
continueOnError
=
false
;
private
String
separator
=
";"
;
private
Charset
encoding
;
/**
* Returns the locations of the DDL (schema) scripts to apply to the database.
* @return the locations of the DDL scripts
*/
public
List
<
String
>
getDdlScriptLocations
()
{
return
this
.
ddlScriptLocations
;
}
/**
* Sets the locations of DDL (schema) scripts to apply to the database. By default,
* initialization will fail if a location does not exist. To prevent a failure, a
* location can be made optional by prefixing it with {@code optional:}.
* @param ddlScriptLocations locations of the DDL scripts
*/
public
void
setDdlScriptLocations
(
List
<
String
>
ddlScriptLocations
)
{
this
.
ddlScriptLocations
=
ddlScriptLocations
;
}
/**
* Returns the locations of the DML (data) scripts to apply to the database.
* @return the locations of the DML scripts
*/
public
List
<
String
>
getDmlScriptLocations
()
{
return
this
.
dmlScriptLocations
;
}
/**
* Sets the locations of DML (data) scripts to apply to the database. By default,
* initialization will fail if a location does not exist. To prevent a failure, a
* location can be made optional by prefixing it with {@code optional:}.
* @param dmlScriptLocations locations of the DML scripts
*/
public
void
setDmlScriptLocations
(
List
<
String
>
dmlScriptLocations
)
{
this
.
dmlScriptLocations
=
dmlScriptLocations
;
}
/**
* Returns whether to continue when an error occurs while applying a DDL or DML
* script.
* @return whether to continue on error
*/
public
boolean
isContinueOnError
()
{
return
this
.
continueOnError
;
}
/**
* Sets whether initialization should continue when an error occurs when applying a
* DDL or DML script.
* @param continueOnError whether to continue when an error occurs.
*/
public
void
setContinueOnError
(
boolean
continueOnError
)
{
this
.
continueOnError
=
continueOnError
;
}
/**
* Returns the statement separator used in the DDL and DML scripts.
* @return the statement separator
*/
public
String
getSeparator
()
{
return
this
.
separator
;
}
/**
* Sets the statement separator to use when reading the DDL and DML scripts.
* @param separator statement separator used in DDL and DML scripts
*/
public
void
setSeparator
(
String
separator
)
{
this
.
separator
=
separator
;
}
/**
* Returns the encoding to use when reading the DDL and DML scripts.
* @return the script encoding
*/
public
Charset
getEncoding
()
{
return
this
.
encoding
;
}
/**
* Sets the encoding to use when reading the DDL and DML scripts.
* @param encoding encoding to use when reading the DDL and DML scripts
*/
public
void
setEncoding
(
Charset
encoding
)
{
this
.
encoding
=
encoding
;
}
}
spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializer.java
0 → 100644
View file @
5dee68c9
/*
* Copyright 2012-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
.
boot
.
jdbc
.
init
;
import
java.io.IOException
;
import
java.nio.charset.Charset
;
import
java.util.ArrayList
;
import
java.util.Arrays
;
import
java.util.Collections
;
import
java.util.List
;
import
javax.sql.DataSource
;
import
org.springframework.beans.factory.InitializingBean
;
import
org.springframework.context.ResourceLoaderAware
;
import
org.springframework.core.io.Resource
;
import
org.springframework.core.io.ResourceLoader
;
import
org.springframework.core.io.support.ResourcePatternResolver
;
import
org.springframework.core.io.support.ResourcePatternUtils
;
import
org.springframework.jdbc.datasource.init.DatabasePopulatorUtils
;
import
org.springframework.jdbc.datasource.init.ResourceDatabasePopulator
;
import
org.springframework.util.CollectionUtils
;
/**
* {@link InitializingBean} that performs {@link DataSource} initialization using DDL and
* DML scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
*/
public
class
ScriptDataSourceInitializer
implements
ResourceLoaderAware
,
InitializingBean
{
private
static
final
String
OPTIONAL_LOCATION_PREFIX
=
"optional:"
;
private
final
DataSource
dataSource
;
private
final
DataSourceInitializationSettings
settings
;
private
volatile
ResourceLoader
resourceLoader
;
/**
* Creates a new {@link ScriptDataSourceInitializer} that will initialize the given
* {@code DataSource} using the given settings.
* @param dataSource data source to initialize
* @param settings initialization settings
*/
public
ScriptDataSourceInitializer
(
DataSource
dataSource
,
DataSourceInitializationSettings
settings
)
{
this
.
dataSource
=
dataSource
;
this
.
settings
=
settings
;
}
/**
* Returns the {@code DataSource} that will be initialized.
* @return the initialization data source
*/
protected
final
DataSource
getDataSource
()
{
return
this
.
dataSource
;
}
@Override
public
void
setResourceLoader
(
ResourceLoader
resourceLoader
)
{
this
.
resourceLoader
=
resourceLoader
;
}
@Override
public
void
afterPropertiesSet
()
throws
Exception
{
initializeDatabase
();
}
/**
* Initializes the database by running DDL and DML scripts.
* @return {@code true} if one or more scripts were applied to the database, otherwise
* {@code false}
*/
public
boolean
initializeDatabase
()
{
ScriptLocationResolver
locationResolver
=
new
ScriptLocationResolver
(
this
.
resourceLoader
);
boolean
initialized
=
applyDdlScripts
(
locationResolver
);
initialized
=
applyDmlScripts
(
locationResolver
)
||
initialized
;
return
initialized
;
}
private
boolean
applyDdlScripts
(
ScriptLocationResolver
locationResolver
)
{
return
applyScripts
(
this
.
settings
.
getDdlScriptLocations
(),
"DDL"
,
locationResolver
);
}
private
boolean
applyDmlScripts
(
ScriptLocationResolver
locationResolver
)
{
return
applyScripts
(
this
.
settings
.
getDmlScriptLocations
(),
"DML"
,
locationResolver
);
}
private
boolean
applyScripts
(
List
<
String
>
locations
,
String
type
,
ScriptLocationResolver
locationResolver
)
{
List
<
Resource
>
scripts
=
getScripts
(
locations
,
type
,
locationResolver
);
if
(!
scripts
.
isEmpty
())
{
runScripts
(
scripts
);
}
return
!
scripts
.
isEmpty
();
}
private
List
<
Resource
>
getScripts
(
List
<
String
>
locations
,
String
type
,
ScriptLocationResolver
locationResolver
)
{
if
(
CollectionUtils
.
isEmpty
(
locations
))
{
return
Collections
.
emptyList
();
}
List
<
Resource
>
resources
=
new
ArrayList
<>();
for
(
String
location
:
locations
)
{
boolean
optional
=
location
.
startsWith
(
OPTIONAL_LOCATION_PREFIX
);
if
(
optional
)
{
location
=
location
.
substring
(
OPTIONAL_LOCATION_PREFIX
.
length
());
}
for
(
Resource
resource
:
doGetResources
(
location
,
locationResolver
))
{
if
(
resource
.
exists
())
{
resources
.
add
(
resource
);
}
else
if
(!
optional
)
{
throw
new
IllegalStateException
(
"No "
+
type
+
" scripts found at location '"
+
location
+
"'"
);
}
}
}
return
resources
;
}
private
List
<
Resource
>
doGetResources
(
String
location
,
ScriptLocationResolver
locationResolver
)
{
try
{
return
locationResolver
.
resolve
(
location
);
}
catch
(
Exception
ex
)
{
throw
new
IllegalStateException
(
"Unable to load resources from "
+
location
,
ex
);
}
}
private
void
runScripts
(
List
<
Resource
>
resources
)
{
if
(
resources
.
isEmpty
())
{
return
;
}
runScripts
(
resources
,
this
.
settings
.
isContinueOnError
(),
this
.
settings
.
getSeparator
(),
this
.
settings
.
getEncoding
());
}
protected
void
runScripts
(
List
<
Resource
>
resources
,
boolean
continueOnError
,
String
separator
,
Charset
encoding
)
{
ResourceDatabasePopulator
populator
=
new
ResourceDatabasePopulator
();
populator
.
setContinueOnError
(
continueOnError
);
populator
.
setSeparator
(
separator
);
if
(
encoding
!=
null
)
{
populator
.
setSqlScriptEncoding
(
encoding
.
name
());
}
for
(
Resource
resource
:
resources
)
{
populator
.
addScript
(
resource
);
}
DatabasePopulatorUtils
.
execute
(
populator
,
this
.
dataSource
);
}
private
static
class
ScriptLocationResolver
{
private
final
ResourcePatternResolver
resourcePatternResolver
;
ScriptLocationResolver
(
ResourceLoader
resourceLoader
)
{
this
.
resourcePatternResolver
=
ResourcePatternUtils
.
getResourcePatternResolver
(
resourceLoader
);
}
private
List
<
Resource
>
resolve
(
String
location
)
throws
IOException
{
List
<
Resource
>
resources
=
new
ArrayList
<>(
Arrays
.
asList
(
this
.
resourcePatternResolver
.
getResources
(
location
)));
resources
.
sort
((
r1
,
r2
)
->
{
try
{
return
r1
.
getURL
().
toString
().
compareTo
(
r2
.
getURL
().
toString
());
}
catch
(
IOException
ex
)
{
return
0
;
}
});
return
resources
;
}
}
}
spring-boot-project/spring-boot
-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization
DataSourceInitializerDetector.java
→
spring-boot-project/spring-boot
/src/main/java/org/springframework/boot/jdbc/init/Script
DataSourceInitializerDetector.java
View file @
5dee68c9
...
...
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
autoconfigure
.
jdbc
;
package
org
.
springframework
.
boot
.
jdbc
.
init
;
import
java.util.Collections
;
import
java.util.Set
;
...
...
@@ -23,15 +23,15 @@ import org.springframework.boot.jdbc.init.dependency.AbstractBeansOfTypeDataSour
import
org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector
;
/**
* A {@link DataSourceInitializerDetector} for {@link
DataSourceInitialization
}.
* A {@link DataSourceInitializerDetector} for {@link
ScriptDataSourceInitializer
}.
*
* @author Andy Wilkinson
*/
class
DataSourceInitialization
DataSourceInitializerDetector
extends
AbstractBeansOfTypeDataSourceInitializerDetector
{
class
Script
DataSourceInitializerDetector
extends
AbstractBeansOfTypeDataSourceInitializerDetector
{
@Override
protected
Set
<
Class
<?>>
getDataSourceInitializerBeanTypes
()
{
return
Collections
.
singleton
(
DataSourceInitialization
.
class
);
return
Collections
.
singleton
(
ScriptDataSourceInitializer
.
class
);
}
}
spring-boot-project/spring-boot
-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jdbc/DataSourceInitialization
.java
→
spring-boot-project/spring-boot
/src/main/java/org/springframework/boot/jdbc/init/package-info
.java
View file @
5dee68c9
...
...
@@ -14,48 +14,7 @@
* limitations under the License.
*/
package
org
.
springframework
.
boot
.
autoconfigure
.
jdbc
;
import
javax.sql.DataSource
;
import
org.springframework.beans.factory.InitializingBean
;
import
org.springframework.context.ResourceLoaderAware
;
import
org.springframework.core.io.ResourceLoader
;
/**
* {@link InitializingBean} that performs {@link DataSource} initialization using DDL and
* DML scripts.
*
* @author Andy Wilkinson
* @since 2.5.0
* Support for initializaton of a JDBC {@code DataSource}.
*/
public
class
DataSourceInitialization
implements
InitializingBean
,
ResourceLoaderAware
{
private
final
DataSource
dataSource
;
private
final
DataSourceProperties
properies
;
private
volatile
ResourceLoader
resourceLoader
;
/**
* Creates a new {@link DataSourceInitialization} that will initialize the given
* {@code DataSource} using the settings from the given {@code properties}.
* @param dataSource the DataSource to initialize
* @param properies the properties containing the initialization settings
*/
public
DataSourceInitialization
(
DataSource
dataSource
,
DataSourceProperties
properies
)
{
this
.
dataSource
=
dataSource
;
this
.
properies
=
properies
;
}
@Override
public
void
afterPropertiesSet
()
throws
Exception
{
new
DataSourceInitializer
(
this
.
dataSource
,
this
.
properies
,
this
.
resourceLoader
).
initializeDataSource
();
}
@Override
public
void
setResourceLoader
(
ResourceLoader
resourceLoader
)
{
this
.
resourceLoader
=
resourceLoader
;
}
}
package
org
.
springframework
.
boot
.
jdbc
.
init
;
spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories
View file @
5dee68c9
...
...
@@ -82,6 +82,7 @@ org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter
# DataSource Initializer Detectors
org.springframework.boot.jdbc.init.dependency.DataSourceInitializerDetector=\
org.springframework.boot.flyway.FlywayDataSourceInitializerDetector,\
org.springframework.boot.jdbc.init.ScriptDataSourceInitializerDetector,\
org.springframework.boot.liquibase.LiquibaseDataSourceInitializerDetector,\
org.springframework.boot.orm.jpa.JpaDataSourceInitializerDetector
...
...
spring-boot-project/spring-boot/src/test/java/org/springframework/boot/jdbc/init/ScriptDataSourceInitializerTests.java
0 → 100644
View file @
5dee68c9
/*
* Copyright 2012-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
.
boot
.
jdbc
.
init
;
import
java.util.Arrays
;
import
java.util.UUID
;
import
com.zaxxer.hikari.HikariDataSource
;
import
org.junit.jupiter.api.AfterEach
;
import
org.junit.jupiter.api.Test
;
import
org.springframework.boot.jdbc.DataSourceBuilder
;
import
org.springframework.dao.DataAccessException
;
import
org.springframework.jdbc.core.JdbcTemplate
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThat
;
import
static
org
.
assertj
.
core
.
api
.
Assertions
.
assertThatExceptionOfType
;
/**
* Tests for {@link ScriptDataSourceInitializer}.
*
* @author Andy Wilkinson
*/
class
ScriptDataSourceInitializerTests
{
private
final
HikariDataSource
dataSource
=
DataSourceBuilder
.
create
().
type
(
HikariDataSource
.
class
)
.
url
(
"jdbc:h2:mem:"
+
UUID
.
randomUUID
()).
build
();
@AfterEach
void
closeDataSource
()
{
this
.
dataSource
.
close
();
}
@Test
void
whenDatabaseIsInitializedThenDdlAndDmlScriptsAreApplied
()
{
DataSourceInitializationSettings
settings
=
new
DataSourceInitializationSettings
();
settings
.
setDdlScriptLocations
(
Arrays
.
asList
(
"schema.sql"
));
settings
.
setDmlScriptLocations
(
Arrays
.
asList
(
"data.sql"
));
ScriptDataSourceInitializer
initializer
=
createInitializer
(
settings
);
assertThat
(
initializer
.
initializeDatabase
()).
isTrue
();
assertThat
(
numberOfRows
(
"SELECT COUNT(*) FROM EXAMPLE"
)).
isEqualTo
(
1
);
}
@Test
void
whenContinueOnErrorIsFalseThenInitializationFailsOnError
()
{
DataSourceInitializationSettings
settings
=
new
DataSourceInitializationSettings
();
settings
.
setDmlScriptLocations
(
Arrays
.
asList
(
"data.sql"
));
ScriptDataSourceInitializer
initializer
=
createInitializer
(
settings
);
assertThatExceptionOfType
(
DataAccessException
.
class
).
isThrownBy
(()
->
initializer
.
initializeDatabase
());
}
@Test
void
whenContinueOnErrorIsTrueThenInitializationDoesNotFailOnError
()
{
DataSourceInitializationSettings
settings
=
new
DataSourceInitializationSettings
();
settings
.
setContinueOnError
(
true
);
settings
.
setDmlScriptLocations
(
Arrays
.
asList
(
"data.sql"
));
ScriptDataSourceInitializer
initializer
=
createInitializer
(
settings
);
assertThat
(
initializer
.
initializeDatabase
()).
isTrue
();
}
private
ScriptDataSourceInitializer
createInitializer
(
DataSourceInitializationSettings
settings
)
{
return
new
ScriptDataSourceInitializer
(
this
.
dataSource
,
settings
);
}
private
int
numberOfRows
(
String
sql
)
{
return
new
JdbcTemplate
(
this
.
dataSource
).
queryForObject
(
sql
,
Integer
.
class
);
}
}
spring-boot-project/spring-boot/src/test/resources/data.sql
0 → 100644
View file @
5dee68c9
INSERT
INTO
EXAMPLE
VALUES
(
1
,
'Andy'
);
\ No newline at end of file
spring-boot-project/spring-boot/src/test/resources/schema.sql
0 → 100644
View file @
5dee68c9
CREATE
TABLE
EXAMPLE
(
id
INTEGER
IDENTITY
PRIMARY
KEY
,
name
VARCHAR
(
30
)
);
\ No newline at end of file
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment