@@ -4,25 +4,25 @@ import com.fasterxml.jackson.databind.ObjectMapper
4
4
import liquibase.Contexts
5
5
import liquibase.LabelExpression
6
6
import liquibase.Liquibase
7
- import liquibase.database.Database
8
- import liquibase.database.DatabaseFactory
9
7
import liquibase.database.jvm.JdbcConnection
8
+ import liquibase.exception.LiquibaseException
10
9
import liquibase.resource.ClassLoaderResourceAccessor
11
10
import net.corda.core.identity.CordaX500Name
12
- import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource
13
11
import net.corda.core.schemas.MappedSchema
14
12
import net.corda.core.utilities.contextLogger
13
+ import net.corda.nodeapi.internal.MigrationHelpers.getMigrationResource
15
14
import net.corda.nodeapi.internal.cordapp.CordappLoader
16
15
import java.io.ByteArrayInputStream
17
16
import java.io.InputStream
18
17
import java.nio.file.Path
18
+ import java.sql.Connection
19
19
import java.sql.Statement
20
- import javax.sql.DataSource
21
20
import java.util.concurrent.locks.ReentrantLock
21
+ import javax.sql.DataSource
22
22
import kotlin.concurrent.withLock
23
23
24
24
// Migrate the database to the current version, using liquibase.
25
- class SchemaMigration (
25
+ open class SchemaMigration (
26
26
val schemas : Set <MappedSchema >,
27
27
val dataSource : DataSource ,
28
28
cordappLoader : CordappLoader ? = null ,
@@ -33,14 +33,16 @@ class SchemaMigration(
33
33
private val ourName : CordaX500Name ? = null ,
34
34
// This parameter forces an error to be thrown if there are missing migrations. When using H2, Hibernate will automatically create schemas where they are
35
35
// missing, so no need to throw unless you're specifically testing whether all the migrations are present.
36
- private val forceThrowOnMissingMigration : Boolean = false ) {
36
+ private val forceThrowOnMissingMigration : Boolean = false ,
37
+ protected val databaseFactory : LiquibaseDatabaseFactory = LiquibaseDatabaseFactoryImpl ()) {
37
38
38
39
companion object {
39
40
private val logger = contextLogger()
40
41
const val NODE_BASE_DIR_KEY = " liquibase.nodeDaseDir"
41
42
const val NODE_X500_NAME = " liquibase.nodeName"
42
43
val loader = ThreadLocal <CordappLoader >()
43
- private val mutex = ReentrantLock ()
44
+ @JvmStatic
45
+ protected val mutex = ReentrantLock ()
44
46
}
45
47
46
48
init {
@@ -49,25 +51,54 @@ class SchemaMigration(
49
51
50
52
private val classLoader = cordappLoader?.appClassLoader ? : Thread .currentThread().contextClassLoader
51
53
52
- /* *
54
+ /* *
53
55
* Will run the Liquibase migration on the actual database.
54
56
*/
55
- fun runMigration (existingCheckpoints : Boolean ) {
56
- migrateOlderDatabaseToUseLiquibase(existingCheckpoints)
57
- doRunMigration(run = true , check = false , existingCheckpoints = existingCheckpoints)
58
- }
57
+ fun runMigration (existingCheckpoints : Boolean ) {
58
+ migrateOlderDatabaseToUseLiquibase(existingCheckpoints)
59
+ val resourcesAndSourceInfo = prepareResources()
60
+
61
+ // current version of Liquibase appears to be non-threadsafe
62
+ // this is apparent when multiple in-process nodes are all running migrations simultaneously
63
+ mutex.withLock {
64
+ dataSource.connection.use { connection ->
65
+ val (runner, _, shouldBlockOnCheckpoints) = prepareRunner(connection, resourcesAndSourceInfo)
66
+ if (shouldBlockOnCheckpoints && existingCheckpoints)
67
+ throw CheckpointsException ()
68
+ try {
69
+ runner.update(Contexts ().toString())
70
+ } catch (exp: LiquibaseException ) {
71
+ throw DatabaseMigrationException (exp.message, exp)
72
+ }
73
+ }
74
+ }
75
+ }
59
76
60
77
/* *
61
78
* Ensures that the database is up to date with the latest migration changes.
62
79
*/
63
- fun checkState () = doRunMigration(run = false , check = true )
80
+ fun checkState () {
81
+ val resourcesAndSourceInfo = prepareResources()
64
82
65
- /* * Create a resourse accessor that aggregates the changelogs included in the schemas into one dynamic stream. */
66
- private class CustomResourceAccessor (val dynamicInclude : String , val changelogList : List <String ?>, classLoader : ClassLoader ) : ClassLoaderResourceAccessor(classLoader) {
83
+ // current version of Liquibase appears to be non-threadsafe
84
+ // this is apparent when multiple in-process nodes are all running migrations simultaneously
85
+ mutex.withLock {
86
+ dataSource.connection.use { connection ->
87
+ val (_, changeToRunCount, _) = prepareRunner(connection, resourcesAndSourceInfo)
88
+ if (changeToRunCount > 0 )
89
+ throw OutstandingDatabaseChangesException (changeToRunCount)
90
+ }
91
+ }
92
+ }
93
+
94
+ /* * Create a resource accessor that aggregates the changelogs included in the schemas into one dynamic stream. */
95
+ protected class CustomResourceAccessor (val dynamicInclude : String , val changelogList : List <String ?>, classLoader : ClassLoader ) :
96
+ ClassLoaderResourceAccessor (classLoader) {
67
97
override fun getResourcesAsStream (path : String ): Set <InputStream > {
68
98
if (path == dynamicInclude) {
69
99
// Create a map in Liquibase format including all migration files.
70
- val includeAllFiles = mapOf (" databaseChangeLog" to changelogList.filter { it != null }.map { file -> mapOf (" include" to mapOf (" file" to file)) })
100
+ val includeAllFiles = mapOf (" databaseChangeLog"
101
+ to changelogList.filterNotNull().map { file -> mapOf (" include" to mapOf (" file" to file)) })
71
102
72
103
// Transform it to json.
73
104
val includeAllFilesJson = ObjectMapper ().writeValueAsBytes(includeAllFiles)
@@ -87,59 +118,40 @@ class SchemaMigration(
87
118
null
88
119
}
89
120
90
- private fun doRunMigration (
91
- run : Boolean ,
92
- check : Boolean ,
93
- existingCheckpoints : Boolean? = null
94
- ) {
95
-
96
- // Virtual file name of the changelog that includes all schemas.
97
- val dynamicInclude = " master.changelog.json"
98
-
99
- dataSource.connection.use { connection ->
100
-
101
- // Collect all changelog files referenced in the included schemas.
102
- val changelogList = schemas.mapNotNull { mappedSchema ->
103
- val resource = getMigrationResource(mappedSchema, classLoader)
104
- when {
105
- resource != null -> resource
106
- // Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised
107
- (mappedSchema::class .qualifiedName == " net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class .qualifiedName == " net.corda.finance.schemas.CommercialPaperSchemaV1" ) && mappedSchema.migrationResource == null -> null
108
- else -> logOrThrowMigrationError(mappedSchema)
109
- }
121
+ // Virtual file name of the changelog that includes all schemas.
122
+ val dynamicInclude = " master.changelog.json"
123
+
124
+ protected fun prepareResources (): List <Pair <CustomResourceAccessor , String >> {
125
+ // Collect all changelog files referenced in the included schemas.
126
+ val changelogList = schemas.mapNotNull { mappedSchema ->
127
+ val resource = getMigrationResource(mappedSchema, classLoader)
128
+ when {
129
+ resource != null -> resource
130
+ // Corda OS FinanceApp in v3 has no Liquibase script, so no error is raised
131
+ (mappedSchema::class .qualifiedName == " net.corda.finance.schemas.CashSchemaV1" || mappedSchema::class .qualifiedName == " net.corda.finance.schemas.CommercialPaperSchemaV1" ) && mappedSchema.migrationResource == null -> null
132
+ else -> logOrThrowMigrationError(mappedSchema)
110
133
}
134
+ }
111
135
112
- val path = currentDirectory?.toString()
113
- if (path != null ) {
114
- System .setProperty(NODE_BASE_DIR_KEY , path) // base dir for any custom change set which may need to load a file (currently AttachmentVersionNumberMigration)
115
- }
116
- if (ourName != null ) {
117
- System .setProperty(NODE_X500_NAME , ourName.toString())
118
- }
119
- val customResourceAccessor = CustomResourceAccessor (dynamicInclude, changelogList, classLoader)
120
- checkResourcesInClassPath(changelogList)
121
-
122
- // current version of Liquibase appears to be non-threadsafe
123
- // this is apparent when multiple in-process nodes are all running migrations simultaneously
124
- mutex.withLock {
125
- val liquibase = Liquibase (dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection (connection)))
126
-
127
- val unRunChanges = liquibase.listUnrunChangeSets(Contexts (), LabelExpression ())
128
-
129
- when {
130
- (run && ! check) && (unRunChanges.isNotEmpty() && existingCheckpoints!! ) -> throw CheckpointsException () // Do not allow database migration when there are checkpoints
131
- run && ! check -> liquibase.update(Contexts ())
132
- check && ! run && unRunChanges.isNotEmpty() -> throw OutstandingDatabaseChangesException (unRunChanges.size)
133
- check && ! run -> {
134
- } // Do nothing will be interpreted as "check succeeded"
135
- else -> throw IllegalStateException (" Invalid usage." )
136
- }
137
- }
136
+ val path = currentDirectory?.toString()
137
+ if (path != null ) {
138
+ System .setProperty(NODE_BASE_DIR_KEY , path) // base dir for any custom change set which may need to load a file (currently AttachmentVersionNumberMigration)
138
139
}
140
+ if (ourName != null ) {
141
+ System .setProperty(NODE_X500_NAME , ourName.toString())
142
+ }
143
+ val customResourceAccessor = CustomResourceAccessor (dynamicInclude, changelogList, classLoader)
144
+ checkResourcesInClassPath(changelogList)
145
+ return listOf (Pair (customResourceAccessor, " " ))
139
146
}
140
147
141
- private fun getLiquibaseDatabase (conn : JdbcConnection ): Database {
142
- return DatabaseFactory .getInstance().findCorrectDatabaseImplementation(conn)
148
+ protected fun prepareRunner (connection : Connection ,
149
+ resourcesAndSourceInfo : List <Pair <CustomResourceAccessor , String >>): Triple <Liquibase , Int , Boolean > {
150
+ require(resourcesAndSourceInfo.size == 1 )
151
+ val liquibase = Liquibase (dynamicInclude, resourcesAndSourceInfo.single().first, databaseFactory.getLiquibaseDatabase(JdbcConnection (connection)))
152
+
153
+ val unRunChanges = liquibase.listUnrunChangeSets(Contexts (), LabelExpression ())
154
+ return Triple (liquibase, unRunChanges.size, ! unRunChanges.isEmpty())
143
155
}
144
156
145
157
/* * For existing database created before verions 4.0 add Liquibase support - creates DATABASECHANGELOG and DATABASECHANGELOGLOCK tables and marks changesets as executed. */
@@ -219,7 +231,7 @@ class SchemaMigration(
219
231
checkResourcesInClassPath(preV4Baseline)
220
232
dataSource.connection.use { connection ->
221
233
val customResourceAccessor = CustomResourceAccessor (dynamicInclude, preV4Baseline, classLoader)
222
- val liquibase = Liquibase (dynamicInclude, customResourceAccessor, getLiquibaseDatabase(JdbcConnection (connection)))
234
+ val liquibase = Liquibase (dynamicInclude, customResourceAccessor, databaseFactory. getLiquibaseDatabase(JdbcConnection (connection)))
223
235
liquibase.changeLogSync(Contexts (), LabelExpression ())
224
236
}
225
237
}
@@ -235,7 +247,7 @@ class SchemaMigration(
235
247
}
236
248
}
237
249
238
- open class DatabaseMigrationException (message : String ) : IllegalArgumentException(message) {
250
+ open class DatabaseMigrationException (message : String? , cause : Throwable ? = null ) : IllegalArgumentException(message, cause ) {
239
251
override val message: String = super .message!!
240
252
}
241
253
0 commit comments