├── playapp ├── public │ ├── stylesheets │ │ └── main.css │ ├── images │ │ └── favicon.png │ └── javascripts │ │ └── jquery-1.9.0.min.js ├── conf │ ├── db │ │ └── migration │ │ │ ├── placeholders │ │ │ ├── V2__add_wows.sql │ │ │ └── V1__create_wow_table.sql │ │ │ ├── default │ │ │ ├── V1__Create_person_table.sql │ │ │ └── V2__Add_people.sql │ │ │ ├── secondary │ │ │ ├── V1__create_job_table.sql │ │ │ └── V2__Add_job.sql │ │ │ └── migration_prefix │ │ │ ├── migration_1__create_project_table.sql │ │ │ └── migration_2__Add_projects.sql │ ├── routes │ └── application.conf ├── app │ ├── views │ │ ├── index.scala.html │ │ └── main.scala.html │ ├── controllers │ │ └── Application.scala │ ├── db │ │ └── migration │ │ │ └── java │ │ │ ├── V2__Add_language.scala │ │ │ └── V1__Create_language_table.scala │ └── loader │ │ └── MyApplicationLoader.scala └── test │ └── PlayModuleSpec.scala ├── project ├── build.properties └── plugins.sbt ├── screenshot1.png ├── screenshot2.png ├── plugin └── src │ ├── test │ ├── resources │ │ └── sample.sql │ └── scala │ │ └── org │ │ └── flywaydb │ │ └── play │ │ ├── WebCommandPathSpec.scala │ │ ├── UrlParserSpec.scala │ │ ├── FileUtilsSpec.scala │ │ └── ConfigReaderSpec.scala │ └── main │ ├── twirl │ └── org │ │ └── flywaydb │ │ └── play │ │ └── views │ │ ├── parts │ │ ├── links.scala.html │ │ ├── header.scala.html │ │ ├── css.scala.html │ │ └── js.scala.html │ │ ├── index.scala.html │ │ └── info.scala.html │ └── scala │ └── org │ └── flywaydb │ └── play │ ├── PlayModule.scala │ ├── FlywayPlayComponents.scala │ ├── FlywayConfiguration.scala │ ├── InvalidDatabaseRevision.scala │ ├── UrlParser.scala │ ├── FileUtils.scala │ ├── WebCommandPath.scala │ ├── FlywayWebCommand.scala │ ├── ConfigReader.scala │ └── PlayInitializer.scala ├── .travis.yml ├── .gitignore ├── LICENSE.txt └── README.md /playapp/public/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.8 2 | -------------------------------------------------------------------------------- /screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitro/flyway-play/master/screenshot1.png -------------------------------------------------------------------------------- /screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitro/flyway-play/master/screenshot2.png -------------------------------------------------------------------------------- /playapp/conf/db/migration/placeholders/V2__add_wows.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO [[[tableName]]] (id, name) VALUES (1, 'Oh!'); 2 | -------------------------------------------------------------------------------- /playapp/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nitro/flyway-play/master/playapp/public/images/favicon.png -------------------------------------------------------------------------------- /plugin/src/test/resources/sample.sql: -------------------------------------------------------------------------------- 1 | create table person ( 2 | id int not null, 3 | name varchar(100) not null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/default/V1__Create_person_table.sql: -------------------------------------------------------------------------------- 1 | create table person ( 2 | id int not null, 3 | name varchar(100) not null 4 | ); -------------------------------------------------------------------------------- /playapp/conf/db/migration/secondary/V1__create_job_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE job ( 2 | id integer primary key, 3 | name varchar(200) not null 4 | ); 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: scala 2 | 3 | jdk: 4 | - oraclejdk8 5 | 6 | scala: 7 | - 2.11.5 8 | 9 | script: 10 | - sbt ++$TRAVIS_SCALA_VERSION playapp/test 11 | 12 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/migration_prefix/migration_1__create_project_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE project ( 2 | id integer primary key, 3 | name varchar(200) not null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/placeholders/V1__create_wow_table.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE [[[tableName]]] ( 2 | id integer primary key, 3 | name varchar(200) [[[maybe]]] null 4 | ); 5 | -------------------------------------------------------------------------------- /playapp/app/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(message: String) 2 | 3 |

4 | @message 5 |

6 | 7 |

8 | This is a test applitcation for flyway plugin 9 |

10 | 11 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/migration_prefix/migration_2__Add_projects.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO project (id, name) VALUES (1, 'Manhattan project'); 2 | INSERT INTO project (id, name) VALUES (2, 'Apollo program'); 3 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/secondary/V2__Add_job.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO job (id, name) VALUES (1, 'Programmer'); 2 | INSERT INTO job (id, name) VALUES (2, 'Teacher'); 3 | INSERT INTO job (id, name) VALUES (3, 'Exorcist'); 4 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/links.scala.html: -------------------------------------------------------------------------------- 1 | @(dbNames: Seq[String]) 2 | 3 | 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | project/project 3 | plugin/project 4 | project/target 5 | target 6 | tmp 7 | .history 8 | dist 9 | /.idea 10 | /*.iml 11 | /out 12 | /.idea_modules 13 | /.classpath 14 | /.project 15 | /RUNNING_PID 16 | /.settings 17 | -------------------------------------------------------------------------------- /playapp/conf/db/migration/default/V2__Add_people.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO person (id, name) VALUES (1, 'John'); 2 | INSERT INTO person (id, name) VALUES (2, 'Paul'); 3 | INSERT INTO person (id, name) VALUES (3, 'George'); 4 | INSERT INTO person (id, name) VALUES (4, 'Ringo'); 5 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/header.scala.html: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/css.scala.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/parts/js.scala.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /playapp/app/controllers/Application.scala: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import javax.inject.Singleton 4 | 5 | import play.api.mvc._ 6 | 7 | @Singleton 8 | class Application extends Controller { 9 | 10 | def index = Action { 11 | Ok(views.html.index("Your new application is ready.")) 12 | } 13 | 14 | def hello = Action { 15 | Ok("Hello") 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /playapp/conf/routes: -------------------------------------------------------------------------------- 1 | # Routes 2 | # This file defines all application routes (Higher priority routes first) 3 | # ~~~~ 4 | 5 | # Home page 6 | GET / controllers.Application.index 7 | GET /hello controllers.Application.hello 8 | 9 | # Map static resources from the /public folder to the /assets URL path 10 | GET /assets/*file controllers.Assets.at(path="/public", file) 11 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/index.scala.html: -------------------------------------------------------------------------------- 1 | @(dbNames: Seq[String]) 2 | 3 | 4 | 5 | play-flyway 6 | @parts.css() 7 | 8 | 9 | @parts.header() 10 |
11 | << Back to app 12 |
13 | @parts.links(dbNames) 14 |
15 |
16 | @parts.js() 17 | 18 | 19 | -------------------------------------------------------------------------------- /playapp/app/db/migration/java/V2__Add_language.scala: -------------------------------------------------------------------------------- 1 | package db.migration.java 2 | 3 | import java.sql.Connection 4 | 5 | import org.flywaydb.core.api.migration.jdbc.JdbcMigration 6 | 7 | class V2__Add_language extends JdbcMigration { 8 | override def migrate(conn: Connection): Unit = { 9 | 10 | conn.createStatement().executeUpdate( 11 | """insert into language(id, name) values(1, 'SQL'); 12 | |insert into language(id, name) values(2, 'Java'); 13 | """.stripMargin) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /playapp/app/db/migration/java/V1__Create_language_table.scala: -------------------------------------------------------------------------------- 1 | package db.migration.java 2 | 3 | import java.sql.Connection 4 | 5 | import org.flywaydb.core.api.migration.jdbc.JdbcMigration 6 | 7 | class V1__Create_language_table extends JdbcMigration { 8 | override def migrate(conn: Connection): Unit = { 9 | 10 | conn.createStatement().executeUpdate( 11 | """create table language ( 12 | | id integer primary key, 13 | | name varchar(100) not null 14 | |);""".stripMargin) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /playapp/app/views/main.scala.html: -------------------------------------------------------------------------------- 1 | @(title: String)(content: Html) 2 | 3 | 4 | 5 | 6 | 7 | @title 8 | 9 | 10 | 11 | 12 | 13 | @content 14 | 15 | 16 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | // The Typesafe repository 2 | resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/" 3 | 4 | // Use the Play sbt plugin for Play projects 5 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.5.0") 6 | 7 | addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.1.1") 8 | 9 | addSbtPlugin("com.typesafe.sbt" % "sbt-scalariform" % "1.3.0") 10 | 11 | scalacOptions ++= Seq("-deprecation", "-unchecked", "-language:_") 12 | 13 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "0.5.0") 14 | 15 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2014 Toshiyuki Takahashi 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 12 | either express or implied. See the License for the specific language 13 | governing permissions and limitations under the License. -------------------------------------------------------------------------------- /playapp/app/loader/MyApplicationLoader.scala: -------------------------------------------------------------------------------- 1 | package loader 2 | 3 | import org.flywaydb.play.FlywayPlayComponents 4 | import play.api._ 5 | import play.api.ApplicationLoader.Context 6 | 7 | class MyApplicationLoader extends ApplicationLoader { 8 | def load(context: Context) = { 9 | new MyComponents(context).application 10 | } 11 | } 12 | 13 | class MyComponents(context: Context) extends BuiltInComponentsFromContext(context) with FlywayPlayComponents { 14 | flywayPlayInitializer 15 | lazy val applicationController = new controllers.Application() 16 | lazy val assets = new controllers.Assets(httpErrorHandler) 17 | lazy val router = new _root_.router.Routes(httpErrorHandler, applicationController, assets) 18 | } -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/PlayModule.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | import play.api.inject._ 20 | 21 | class PlayModule extends Module { 22 | def bindings(environment: Environment, configuration: Configuration) = { 23 | Seq( 24 | bind[PlayInitializer].toSelf.eagerly 25 | ) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FlywayPlayComponents.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | import play.core._ 20 | 21 | trait FlywayPlayComponents { 22 | def configuration: Configuration 23 | def environment: Environment 24 | def webCommands: WebCommands 25 | 26 | lazy val flywayPlayInitializer = new PlayInitializer(configuration, environment, webCommands) 27 | } 28 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FlywayConfiguration.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | case class FlywayConfiguration( 19 | database: DatabaseConfiguration, 20 | auto: Boolean, 21 | initOnMigrate: Boolean, 22 | validateOnMigrate: Boolean, 23 | encoding: String, 24 | placeholderPrefix: Option[String], 25 | placeholderSuffix: Option[String], 26 | placeholders: Map[String, String], 27 | outOfOrder: Boolean, 28 | schemas: List[String], 29 | locations: List[String], 30 | sqlMigrationPrefix: Option[String]) 31 | 32 | case class DatabaseConfiguration( 33 | driver: String, 34 | url: String, 35 | user: String, 36 | password: String) 37 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/WebCommandPathSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import org.scalatest.{ ShouldMatchers, FunSpec } 19 | 20 | class WebCommandPathSpec extends FunSpec with ShouldMatchers { 21 | 22 | describe("PluginConfiguration") { 23 | 24 | describe("migratePath") { 25 | it("construct path to apply migration") { 26 | WebCommandPath.migratePath("foo") should be("/@flyway/foo/migrate") 27 | } 28 | it("extract db to migrate migration") { 29 | val dbName = "/@flyway/foo/migrate" match { 30 | case WebCommandPath.migratePath(db) => Some(db) 31 | case _ => None 32 | } 33 | dbName should be(Some("foo")) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/InvalidDatabaseRevision.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | 20 | case class InvalidDatabaseRevision(db: String, script: String) extends PlayException.RichDescription( 21 | "Database '" + db + "' needs migration!", 22 | "An SQL script need to be run on your database.") { 23 | 24 | def subTitle = "This SQL script must be run:" 25 | def content = script 26 | 27 | private val redirectToApply = s""" 28 | document.location = '${WebCommandPath.migratePath(db)}/?redirect=' + encodeURIComponent(location); 29 | """ 30 | 31 | private val redirectToAdmin = s""" 32 | document.location = '/@flyway/' + encodeURIComponent('${db}') 33 | """ 34 | 35 | def htmlDescription = { 36 | An SQL script will be run on your database - 37 | 38 | 39 | }.mkString 40 | } 41 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/UrlParserSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package org.flywaydb.play 16 | 17 | import org.scalatest.{ ShouldMatchers, FunSpec } 18 | import play.api.Environment 19 | 20 | class UrlParserSpec extends FunSpec with ShouldMatchers { 21 | 22 | val urlParser = new UrlParser(Environment.simple()) 23 | 24 | describe("UrlParser") { 25 | 26 | it("should parse URI that starts with 'postgres:'") { 27 | urlParser.parseUrl("postgres://john:secret@host.example.com/dbname") should be( 28 | ("jdbc:postgresql://host.example.com/dbname", Some("john"), Some("secret"))) 29 | } 30 | 31 | it("should parse URI that starts with 'mysql:' and has no extra parameters") { 32 | urlParser.parseUrl("mysql://john:secret@host.example.com/dbname") should be( 33 | ("jdbc:mysql://host.example.com/dbname?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8_general_ci", Some("john"), Some("secret"))) 34 | } 35 | 36 | it("should parse URI that starts with 'mysql:' and has parameter(s)") { 37 | urlParser.parseUrl("mysql://john:secret@host.example.com/dbname?foo=bar") should be( 38 | ("jdbc:mysql://host.example.com/dbname?foo=bar", Some("john"), Some("secret"))) 39 | } 40 | 41 | it("should return as is for URIs other than 'postgres' or 'mysql' ones") { 42 | urlParser.parseUrl("jdbc:yoursql://host.example.com/dbname") should be( 43 | ("jdbc:yoursql://host.example.com/dbname", None, None)) 44 | } 45 | 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/UrlParser.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api.{ Environment, Mode } 19 | 20 | /** 21 | * Most of the code is taken from package play.api.db.DB. 22 | */ 23 | class UrlParser(environment: Environment) { 24 | val PostgresFullUrl = "^postgres://([a-zA-Z0-9_]+):([^@]+)@([^/]+)/([^\\s]+)$".r 25 | val MysqlFullUrl = "^mysql://([a-zA-Z0-9_]+):([^@]+)@([^/]+)/([^\\s]+)$".r 26 | val MysqlCustomProperties = ".*\\?(.*)".r 27 | val H2DefaultUrl = "^jdbc:h2:mem:.+".r 28 | 29 | def parseUrl(url: String): Tuple3[String, Option[String], Option[String]] = { 30 | 31 | url match { 32 | case PostgresFullUrl(username, password, host, dbname) => 33 | ("jdbc:postgresql://%s/%s".format(host, dbname), Some(username), Some(password)) 34 | case url @ MysqlFullUrl(username, password, host, dbname) => 35 | val defaultProperties = """?useUnicode=yes&characterEncoding=UTF-8&connectionCollation=utf8_general_ci""" 36 | val addDefaultPropertiesIfNeeded = MysqlCustomProperties.findFirstMatchIn(url).map(_ => "").getOrElse(defaultProperties) 37 | ("jdbc:mysql://%s/%s".format(host, dbname + addDefaultPropertiesIfNeeded), Some(username), Some(password)) 38 | case url @ H2DefaultUrl() if !url.contains("DB_CLOSE_DELAY") => 39 | val jdbcUrl = if (environment.mode == Mode.Dev) { 40 | url + ";DB_CLOSE_DELAY=-1" 41 | } else { 42 | url 43 | } 44 | (jdbcUrl, None, None) 45 | case s: String => 46 | (s, None, None) 47 | } 48 | 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /plugin/src/main/twirl/org/flywaydb/play/views/info.scala.html: -------------------------------------------------------------------------------- 1 | @(request: play.api.mvc.RequestHeader, 2 | dbName: String, 3 | allMigrationInfo: Seq[org.flywaydb.core.api.MigrationInfo], 4 | scripts: Seq[String]) 5 | 6 | @import org.flywaydb.play.WebCommandPath 7 | 8 | @withRedirectParam(path: String) = { 9 | @{path}?redirect=@{java.net.URLEncoder.encode(request.path, "utf-8")} 10 | } 11 | 12 | @statusText(info: org.flywaydb.core.api.MigrationInfo) = { 13 | @if(info.getState.isApplied) { 14 | applied 15 | } else { 16 | @if(info.getState.isResolved) { 17 | resolved 18 | } 19 | } 20 | @if(info.getState.isFailed) { 21 | failed 22 | } 23 | } 24 | 25 | 26 | 27 | play-flyway 28 | @parts.css() 29 | 30 | 31 | @parts.header() 32 |
33 | << Back to app 34 |

Database: @dbName

35 | migrate 36 | repair 37 | clean 38 | 39 |
40 | 43 | 52 |
53 | @allMigrationInfo.zip(scripts).map { case (info, script) => 54 |

55 |

56 | @info.getScript (@statusText(info)) 57 |

58 |
@script
59 |

60 | } 61 |
62 | @parts.js() 63 | 64 | 65 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FileUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import java.io.{ File, InputStream } 19 | 20 | object FileUtils { 21 | 22 | def readFileToString(filename: File): String = { 23 | val src = scala.io.Source.fromFile(filename, "UTF-8") 24 | try { 25 | src.mkString 26 | } finally { 27 | src.close() 28 | } 29 | } 30 | 31 | def readInputStreamToString(in: InputStream): String = { 32 | val src = scala.io.Source.fromInputStream(in, "UTF-8") 33 | try { 34 | src.mkString 35 | } finally { 36 | src.close() 37 | in.close() 38 | } 39 | } 40 | 41 | def recursiveListFiles(root: File): Seq[File] = { 42 | if (!root.isDirectory) { 43 | throw new IllegalArgumentException(s"root is not a directory") 44 | } 45 | val these = root.listFiles.toSeq 46 | these ++ these.filter(_.isDirectory).flatMap(recursiveListFiles).toSeq 47 | } 48 | 49 | def findFile(root: File, filename: String): Option[File] = { 50 | recursiveListFiles(root).dropWhile(f => f.getName != filename).headOption 51 | } 52 | 53 | private def findSourceFile(root: File, className: String, ext: String): Option[File] = { 54 | for { 55 | cls <- className.split("\\.").lastOption 56 | filename = cls + "." + ext 57 | f <- findFile(root, filename) 58 | } yield f 59 | } 60 | 61 | private def findJavaSourceFile(root: File, className: String): Option[File] = { 62 | findSourceFile(root, className, "java") 63 | } 64 | 65 | private def findScalaSourceFile(root: File, className: String): Option[File] = { 66 | findSourceFile(root, className, "scala") 67 | } 68 | 69 | def findJdbcMigrationFile(root: File, className: String): Option[File] = { 70 | findScalaSourceFile(root, className).orElse(findJavaSourceFile(root, className)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /playapp/conf/application.conf: -------------------------------------------------------------------------------- 1 | # This is the main configuration file for the application. 2 | # ~~~~~ 3 | 4 | # Secret key 5 | # ~~~~~ 6 | # The secret key is used to secure cryptographics functions. 7 | # If you deploy your application to several instances be sure to use the same key! 8 | application.secret="xU/2nEHY?Ot85T][Ix_Sh;yPo;adCA1Na None 78 | case Some(matched) => Some(matched.group(1), matched.group(2)) 79 | } 80 | } 81 | } 82 | 83 | object initPath { 84 | 85 | def apply(dbName: String): String = { 86 | s"/@flyway/${dbName}/init" 87 | } 88 | 89 | def unapply(path: String): Option[String] = { 90 | initPathRegex.findFirstMatchIn(path).map(_.group(1)) 91 | } 92 | 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/FlywayWebCommand.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import org.flywaydb.core.api.MigrationInfo 19 | import play.core._ 20 | import play.api._ 21 | import play.api.mvc._ 22 | import play.api.mvc.Results._ 23 | import org.flywaydb.core.Flyway 24 | 25 | class FlywayWebCommand( 26 | configuration: Configuration, 27 | environment: Environment, 28 | flywayPrefixToMigrationScript: String, 29 | flyways: Map[String, Flyway]) 30 | extends HandleWebCommandSupport { 31 | 32 | def handleWebCommand(request: RequestHeader, sbtLink: BuildLink, path: java.io.File): Option[Result] = { 33 | 34 | request.path match { 35 | case WebCommandPath.migratePath(dbName) => 36 | for { 37 | flyway <- flyways.get(dbName) 38 | } yield { 39 | flyway.migrate() 40 | sbtLink.forceReload() 41 | Redirect(getRedirectUrlFromRequest(request)) 42 | } 43 | case WebCommandPath.cleanPath(dbName) => 44 | flyways.get(dbName).foreach(_.clean()) 45 | Some(Redirect(getRedirectUrlFromRequest(request))) 46 | case WebCommandPath.repairPath(dbName) => 47 | flyways.get(dbName).foreach(_.repair()) 48 | Some(Redirect(getRedirectUrlFromRequest(request))) 49 | case WebCommandPath.versionedInitPath(dbName, version) => 50 | flyways.get(dbName).foreach(_.setBaselineVersionAsString(version)) 51 | flyways.get(dbName).foreach(_.baseline()) 52 | Some(Redirect(getRedirectUrlFromRequest(request))) 53 | case WebCommandPath.showInfoPath(dbName) => 54 | val allMigrationInfo: Seq[MigrationInfo] = flyways.get(dbName).toSeq.flatMap(_.info().all()) 55 | val scripts: Seq[String] = allMigrationInfo.map { info => 56 | environment.resourceAsStream(s"${flywayPrefixToMigrationScript}/${dbName}/${info.getScript}").map { in => 57 | FileUtils.readInputStreamToString(in) 58 | }.orElse { 59 | for { 60 | script <- FileUtils.findJdbcMigrationFile(environment.rootPath, info.getScript) 61 | } yield FileUtils.readFileToString(script) 62 | }.getOrElse("") 63 | } 64 | Some(Ok(views.html.info(request, dbName, allMigrationInfo, scripts)).as("text/html")) 65 | case "/@flyway" => 66 | Some(Ok(views.html.index(flyways.keys.toSeq)).as("text/html")) 67 | case _ => 68 | None 69 | } 70 | } 71 | 72 | private def getRedirectUrlFromRequest(request: RequestHeader): String = { 73 | (for { 74 | urls <- request.queryString.get("redirect") 75 | url <- urls.headOption 76 | } yield url).getOrElse("/") 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /playapp/test/PlayModuleSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.github.tototoshi.play2.flyway 17 | 18 | import org.scalatest.{ FunSpec, ShouldMatchers } 19 | import play.api.test._ 20 | import play.api.test.Helpers._ 21 | import scalikejdbc._ 22 | import scalikejdbc.SQLInterpolation._ 23 | 24 | class PlayModuleSpec extends FunSpec with ShouldMatchers { 25 | 26 | def fixture = new { 27 | 28 | } 29 | 30 | def test() = { 31 | DB autoCommit { implicit session => 32 | 33 | val people = 34 | sql"SELECT * FROM person" 35 | .map(rs => rs.int("id") -> rs.string("name")) 36 | .list 37 | .apply() 38 | 39 | people.size should be(4) 40 | 41 | sql"DROP TABLE person".execute.apply() 42 | 43 | // Table created by flyway 44 | sql"""DROP TABLE "schema_version"""".execute.apply() 45 | } 46 | 47 | NamedDB('secondary) autoCommit { implicit session => 48 | val person = 49 | sql"SELECT * FROM job" 50 | .map(rs => rs.int("id") -> rs.string("name")) 51 | .list 52 | .apply() 53 | 54 | person.size should be(3) 55 | 56 | sql"DROP TABLE job".execute.apply() 57 | 58 | // Table created by flyway 59 | sql"""DROP TABLE "schema_version"""".execute.apply() 60 | } 61 | 62 | NamedDB('placeholders) autoCommit { implicit session => 63 | val wows = 64 | sql"SELECT * FROM wow" // This table name is substituted for a placeholder during migration 65 | .map(rs => rs.int("id") -> rs.string("name")) 66 | .list 67 | .apply() 68 | 69 | wows.size should be(1) 70 | wows.head should be((1, "Oh!")) 71 | 72 | sql"DROP TABLE wow".execute.apply() 73 | 74 | // Table created by flyway 75 | sql"""DROP TABLE "schema_version"""".execute.apply() 76 | } 77 | 78 | NamedDB('java) autoCommit { implicit session => 79 | val languages = 80 | sql"SELECT * FROM language" 81 | .map(rs => rs.int("id") -> rs.string("name")) 82 | .list 83 | .apply() 84 | 85 | languages.size should be(2) 86 | 87 | sql"DROP TABLE language".execute.apply() 88 | 89 | // Table created by flyway 90 | sql"""DROP TABLE "schema_version"""".execute.apply() 91 | } 92 | 93 | NamedDB('migration_prefix) autoCommit { implicit session => 94 | val projects = 95 | sql"SELECT * FROM project" 96 | .map(rs => rs.int("id") -> rs.string("name")) 97 | .list 98 | .apply() 99 | 100 | projects.size should be(2) 101 | 102 | sql"DROP TABLE project".execute.apply() 103 | 104 | // Table created by flyway 105 | sql"""DROP TABLE "schema_version"""".execute.apply() 106 | } 107 | 108 | } 109 | 110 | describe("PlayModule") { 111 | 112 | it("should migrate automatically when testing") { 113 | running(FakeApplication()) { 114 | test() 115 | } 116 | } 117 | 118 | it("should work fine with in-memory databases.") { 119 | 120 | running(FakeApplication( 121 | additionalConfiguration = 122 | inMemoryDatabase(name = "default", Map("DB_CLOSE_DELAY" -> "-1")) ++ 123 | inMemoryDatabase(name = "secondary", Map("DB_CLOSE_DELAY" -> "-1")) 124 | )) { 125 | test() 126 | } 127 | } 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/FileUtilsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import org.scalatest.{ ShouldMatchers, FunSpec } 19 | import java.io.{ File, FileInputStream } 20 | 21 | class FileUtilsSpec extends FunSpec with ShouldMatchers { 22 | 23 | describe("FileUtils") { 24 | 25 | it("should read a File to String") { 26 | val f = new File("plugin/src/test/resources/sample.sql") 27 | val s = FileUtils.readFileToString(f) 28 | s should be("""|create table person ( 29 | | id int not null, 30 | | name varchar(100) not null 31 | |); 32 | |""".stripMargin) 33 | } 34 | 35 | it("should read InputStream to String") { 36 | val f = new FileInputStream("plugin/src/test/resources/sample.sql") 37 | val s = FileUtils.readInputStreamToString(f) 38 | s should be("""|create table person ( 39 | | id int not null, 40 | | name varchar(100) not null 41 | |); 42 | |""".stripMargin) 43 | } 44 | 45 | it("should find files recursively") { 46 | val temp = File.createTempFile("flyway-play-", "-test"); 47 | temp.delete() 48 | temp.mkdir() 49 | val sub1 = new File(temp, "sub1") 50 | sub1.mkdir() 51 | val sub2 = new File(sub1, "sub2") 52 | sub2.mkdir() 53 | val testfile1 = new File(sub2, "AAA.java") 54 | testfile1.createNewFile() 55 | val testfile2 = new File(sub2, "BBB.scala") 56 | testfile2.createNewFile() 57 | 58 | FileUtils.recursiveListFiles(temp) should contain theSameElementsAs ( 59 | Seq(sub1, sub2, testfile1, testfile2) 60 | ) 61 | 62 | testfile1.delete() 63 | testfile2.delete() 64 | sub2.delete() 65 | sub1.delete() 66 | temp.delete() 67 | } 68 | 69 | it("should find a file in file tree") { 70 | val temp = File.createTempFile("flyway-play-", "-test"); 71 | temp.delete() 72 | temp.mkdir() 73 | val sub1 = new File(temp, "sub1") 74 | sub1.mkdir() 75 | val sub2 = new File(sub1, "sub2") 76 | sub2.mkdir() 77 | val testfile1 = new File(sub2, "AAA.java") 78 | testfile1.createNewFile() 79 | val testfile2 = new File(sub2, "BBB.scala") 80 | testfile2.createNewFile() 81 | 82 | FileUtils.findFile(temp, "AAA.java") should be(Some(testfile1)) 83 | 84 | testfile1.delete() 85 | testfile2.delete() 86 | sub2.delete() 87 | sub1.delete() 88 | temp.delete() 89 | } 90 | 91 | it("should find a java/scala file in file tree") { 92 | val temp = File.createTempFile("flyway-play-", "-test"); 93 | temp.delete() 94 | temp.mkdir() 95 | val sub1 = new File(temp, "sub1") 96 | sub1.mkdir() 97 | val sub2 = new File(sub1, "sub2") 98 | sub2.mkdir() 99 | val testfile1 = new File(sub2, "AAA.java") 100 | testfile1.createNewFile() 101 | val testfile2 = new File(sub2, "BBB.scala") 102 | testfile2.createNewFile() 103 | 104 | FileUtils.findJdbcMigrationFile(temp, "org.flywaydb.flyway.AAA") should be(Some(testfile1)) 105 | FileUtils.findJdbcMigrationFile(temp, "org.flywaydb.flyway.BBB") should be(Some(testfile2)) 106 | 107 | testfile1.delete() 108 | testfile2.delete() 109 | sub2.delete() 110 | sub1.delete() 111 | temp.delete() 112 | } 113 | 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/ConfigReader.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api._ 19 | import scala.collection.JavaConverters._ 20 | 21 | class ConfigReader(configuration: Configuration, environment: Environment) { 22 | 23 | case class JdbcConfig(driver: String, url: String, username: String, password: String) 24 | 25 | val urlParser = new UrlParser(environment: Environment) 26 | 27 | val logger = Logger(classOf[ConfigReader]) 28 | 29 | private def getAllDatabaseNames: Seq[String] = (for { 30 | config <- configuration.getConfig("db").toList 31 | dbName <- config.subKeys 32 | } yield { 33 | dbName 34 | }).distinct 35 | 36 | def getFlywayConfigurations: Map[String, FlywayConfiguration] = { 37 | (for { 38 | dbName <- getAllDatabaseNames 39 | jdbc <- getJdbcConfig(configuration, dbName) 40 | subConfig = configuration.getConfig(s"db.$dbName.migration").getOrElse(Configuration.empty) 41 | } yield { 42 | val initOnMigrate = subConfig.getBoolean("initOnMigrate").getOrElse(false) 43 | val validateOnMigrate = subConfig.getBoolean("validateOnMigrate").getOrElse(true) 44 | val encoding = subConfig.getString("encoding").getOrElse("UTF-8") 45 | val placeholderPrefix = subConfig.getString("placeholderPrefix") 46 | val placeholderSuffix = subConfig.getString("placeholderSuffix") 47 | 48 | val placeholders = { 49 | subConfig.getConfig("placeholders").map { config => 50 | config.subKeys.map { key => (key -> config.getString(key).getOrElse("")) }.toMap 51 | }.getOrElse(Map.empty) 52 | } 53 | 54 | val outOfOrder = subConfig.getBoolean("outOfOrder").getOrElse(false) 55 | val auto = subConfig.getBoolean("auto").getOrElse(false) 56 | val schemas = subConfig.getStringList("schemas").getOrElse(java.util.Collections.emptyList[String]).asScala.toList 57 | val locations = subConfig.getStringList("locations").getOrElse(java.util.Collections.emptyList[String]).asScala.toList 58 | val sqlMigrationPrefix = subConfig.getString("sqlMigrationPrefix") 59 | 60 | val database = DatabaseConfiguration( 61 | jdbc.driver, 62 | jdbc.url, 63 | jdbc.username, 64 | jdbc.password) 65 | 66 | dbName -> FlywayConfiguration( 67 | database, 68 | auto, 69 | initOnMigrate, 70 | validateOnMigrate, 71 | encoding, 72 | placeholderPrefix, 73 | placeholderSuffix, 74 | placeholders, 75 | outOfOrder, 76 | schemas, 77 | locations, 78 | sqlMigrationPrefix 79 | ) 80 | }).toMap 81 | } 82 | 83 | private def getJdbcConfig(configuration: Configuration, dbName: String): Option[JdbcConfig] = { 84 | val jdbcConfigOrError = for { 85 | jdbcUrl <- configuration.getString(s"db.${dbName}.url").toRight(s"db.$dbName.url is not set").right 86 | driver <- configuration.getString(s"db.${dbName}.driver").toRight(s"db.$dbName.driver is not set").right 87 | } yield { 88 | val (parsedUrl, parsedUser, parsedPass) = urlParser.parseUrl(jdbcUrl) 89 | val username = parsedUser 90 | .orElse(configuration.getString(s"db.${dbName}.username")) 91 | .orElse(configuration.getString(s"db.${dbName}.user")) 92 | .orNull 93 | val password = parsedPass 94 | .orElse(configuration.getString(s"db.${dbName}.password")) 95 | .orElse(configuration.getString(s"db.${dbName}.pass")) 96 | .orNull 97 | JdbcConfig(driver, parsedUrl, username, password) 98 | } 99 | 100 | jdbcConfigOrError match { 101 | case Left(message) => 102 | logger.warn(message) 103 | None 104 | case Right(jdbcConfig) => 105 | Some(jdbcConfig) 106 | } 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /plugin/src/main/scala/org/flywaydb/play/PlayInitializer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import java.io.FileNotFoundException 19 | import javax.inject._ 20 | 21 | import org.flywaydb.core.Flyway 22 | import org.flywaydb.core.api.MigrationInfo 23 | import org.flywaydb.core.internal.util.jdbc.DriverDataSource 24 | import play.api._ 25 | import play.core._ 26 | 27 | import scala.collection.JavaConverters._ 28 | 29 | @Singleton 30 | class PlayInitializer @Inject() ( 31 | configuration: Configuration, 32 | environment: Environment, 33 | webCommands: WebCommands) { 34 | 35 | private val flywayConfigurations = { 36 | val configReader = new ConfigReader(configuration, environment) 37 | configReader.getFlywayConfigurations 38 | } 39 | 40 | private val allDatabaseNames = flywayConfigurations.keys 41 | 42 | private val flywayPrefixToMigrationScript = "db/migration" 43 | 44 | private def migrationFileDirectoryExists(path: String): Boolean = { 45 | environment.resource(path) match { 46 | case Some(r) => { 47 | Logger.debug(s"Directory for migration files found. ${path}") 48 | true 49 | } 50 | case None => { 51 | Logger.warn(s"Directory for migration files not found. ${path}") 52 | false 53 | } 54 | } 55 | } 56 | 57 | private lazy val flyways: Map[String, Flyway] = { 58 | for { 59 | (dbName, configuration) <- flywayConfigurations 60 | migrationFilesLocation = s"${flywayPrefixToMigrationScript}/${dbName}" 61 | if migrationFileDirectoryExists(migrationFilesLocation) 62 | } yield { 63 | val flyway = new Flyway 64 | val database = configuration.database 65 | flyway.setDataSource(new DriverDataSource(getClass.getClassLoader, database.driver, database.url, database.user, database.password)) 66 | if (!configuration.locations.isEmpty) { 67 | val locations = configuration.locations.map(location => s"${migrationFilesLocation}/${location}") 68 | flyway.setLocations(locations: _*) 69 | } else { 70 | flyway.setLocations(migrationFilesLocation) 71 | } 72 | flyway.setValidateOnMigrate(configuration.validateOnMigrate) 73 | flyway.setEncoding(configuration.encoding) 74 | flyway.setOutOfOrder(configuration.outOfOrder) 75 | if (configuration.initOnMigrate) { 76 | flyway.setBaselineOnMigrate(true) 77 | } 78 | for (prefix <- configuration.placeholderPrefix) { 79 | flyway.setPlaceholderPrefix(prefix) 80 | } 81 | for (suffix <- configuration.placeholderSuffix) { 82 | flyway.setPlaceholderSuffix(suffix) 83 | } 84 | flyway.setSchemas(configuration.schemas: _*) 85 | flyway.setPlaceholders(configuration.placeholders.asJava) 86 | configuration.sqlMigrationPrefix.foreach { sqlMigrationPrefix => 87 | flyway.setSqlMigrationPrefix(sqlMigrationPrefix) 88 | } 89 | 90 | dbName -> flyway 91 | } 92 | } 93 | 94 | private def migrationDescriptionToShow(dbName: String, migration: MigrationInfo): String = { 95 | val locations = flywayConfigurations(dbName).locations 96 | (if (locations.nonEmpty) { 97 | locations.map(location => environment.resourceAsStream(s"${flywayPrefixToMigrationScript}/${dbName}/${location}/${migration.getScript}")) 98 | .find(resource => resource.nonEmpty).getOrElse(None) 99 | } else { 100 | environment.resourceAsStream(s"${flywayPrefixToMigrationScript}/${dbName}/${migration.getScript}") 101 | }).map { in => 102 | s"""|--- ${migration.getScript} --- 103 | |${FileUtils.readInputStreamToString(in)}""".stripMargin 104 | }.orElse { 105 | import scala.util.control.Exception._ 106 | val code = for { 107 | script <- FileUtils.findJdbcMigrationFile(environment.rootPath, migration.getScript) 108 | } yield FileUtils.readFileToString(script) 109 | allCatch opt { environment.classLoader.loadClass(migration.getScript) } map { cls => 110 | s"""|--- ${migration.getScript} --- 111 | |$code""".stripMargin 112 | } 113 | }.getOrElse(throw new FileNotFoundException(s"Migration file not found. ${migration.getScript}")) 114 | } 115 | 116 | private def checkState(dbName: String): Unit = { 117 | flyways.get(dbName).foreach { flyway => 118 | val pendingMigrations = flyway.info().pending 119 | if (!pendingMigrations.isEmpty) { 120 | throw InvalidDatabaseRevision( 121 | dbName, 122 | pendingMigrations.map(migration => migrationDescriptionToShow(dbName, migration)).mkString("\n")) 123 | } 124 | } 125 | } 126 | 127 | def onStart(): Unit = { 128 | val flywayWebCommand = new FlywayWebCommand(configuration, environment, flywayPrefixToMigrationScript, flyways) 129 | webCommands.addHandler(flywayWebCommand) 130 | 131 | for (dbName <- allDatabaseNames) { 132 | if (environment.mode == Mode.Test || flywayConfigurations(dbName).auto) { 133 | migrateAutomatically(dbName) 134 | } else { 135 | checkState(dbName) 136 | } 137 | } 138 | } 139 | 140 | private def migrateAutomatically(dbName: String): Unit = { 141 | flyways.get(dbName).foreach { flyway => 142 | flyway.migrate() 143 | } 144 | } 145 | 146 | val enabled: Boolean = 147 | !configuration.getString("flywayplugin").exists(_ == "disabled") 148 | 149 | if (enabled) { 150 | onStart() 151 | } 152 | 153 | } 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flyway-play 2 | 3 | [![Build Status](https://travis-ci.org/flyway/flyway-play.svg?branch=master)](https://travis-ci.org/flyway/flyway-play) 4 | 5 | Flyway module for Play 2.4 or later. It aims to be a substitute for play-evolutions. 6 | 7 | This module is successor of [tototoshi/play-flyway](https://github.com/tototoshi/play-flyway), which is a Play Plugin supporting Play 2.1 ~ 2.3. 8 | 9 | ## Features 10 | 11 | - Based on [Flyway](https://flywaydb.org/) 12 | - No 'Downs' part. 13 | - Independent of DBPlugin(play.api.db). 14 | 15 | ## Install 16 | 17 | build.sbt 18 | 19 | ```scala 20 | libraryDependencies ++= Seq( 21 | "org.flywaydb" %% "flyway-play" % "3.0.1" 22 | ) 23 | ``` 24 | 25 | conf/application.conf 26 | 27 | ``` 28 | play.modules.enabled += "org.flywaydb.play.PlayModule" 29 | ``` 30 | 31 | 32 | ## Getting Started 33 | 34 | ### Basic configuration 35 | 36 | Database settings can be set in manner of Play2. 37 | 38 | ``` 39 | db.default.driver=org.h2.Driver 40 | db.default.url="jdbc:h2:mem:example2;db_CLOSE_DELAY=-1" 41 | db.default.username="sa" 42 | db.default.password="secret" 43 | 44 | # optional 45 | db.default.schemas=["public", "other"] 46 | ``` 47 | 48 | ### Place migration scripts 49 | 50 | A migration script is just a simple SQL file. 51 | 52 | ```sql 53 | CREATE TABLE FOO (............. 54 | 55 | 56 | ``` 57 | 58 | By default place your migration scripts in `conf/db/migration/${dbName}` . 59 | 60 | ``` 61 | playapp 62 | ├── app 63 | │ ├── controllers 64 | │ ├── models 65 | │ └── views 66 | ├── conf 67 | │ ├── application.conf 68 | │ ├── db 69 | │ │ └── migration 70 | │ │ ├── default 71 | │ │ │ ├── V1__Create_person_table.sql 72 | │ │ │ └── V2__Add_people.sql 73 | │ │ └── secondary 74 | │ │ ├── V1__create_job_table.sql 75 | │ │ └── V2__Add_job.sql 76 | │ ├── play.plugins 77 | │ └── routes 78 | ``` 79 | 80 | Alternatively, specify one or more locations per database and place your migrations in `conf/db/migration/${dbName}/${locations[1...N]}` . By varying the locations in each environment you are able to specify different scripts per RDBMS for each upgrade. These differences should be kept minimal. 81 | 82 | For example, in testing use the configuration: 83 | 84 | ``` 85 | db.default.migration.locations=["common","h2"] 86 | ``` 87 | 88 | And in production use the configuration: 89 | 90 | ``` 91 | db.default.migration.locations=["common","mysql"] 92 | ``` 93 | 94 | Then put your migrations in these folders. Note that the migrations for the `secondary` database remain in the default location. 95 | 96 | ``` 97 | playapp 98 | ├── app 99 | │ ├── controllers 100 | │ ├── models 101 | │ └── views 102 | ├── conf 103 | │ ├── application.conf 104 | │ ├── db 105 | │ │ └── migration 106 | │ │ ├── default 107 | │ │ │ ├── common 108 | │ │ │ │ └── V2__Add_people.sql 109 | │ │ │ ├── h2 110 | │ │ │ │ └── V1__Create_person_table.sql 111 | │ │ │ ├── mysql 112 | │ │ │ │ └── V1__Create_person_table.sql 113 | │ │ └── secondary 114 | │ │ ├── V1__create_job_table.sql 115 | │ │ └── V2__Add_job.sql 116 | │ ├── play.plugins 117 | │ └── routes 118 | ``` 119 | 120 | 121 | Please see flyway's documents about the naming convention for migration scripts. 122 | 123 | https://flywaydb.org/documentation/migration/sql.html 124 | 125 | ### Placeholders 126 | 127 | Flyway can replace placeholders in Sql migrations. 128 | The default pattern is ${placeholder}. 129 | This can be configured using the placeholderPrefix and placeholderSuffix properties. 130 | 131 | The placeholder prefix, suffix and key-value pairs can be specificed in application.conf, e.g. 132 | 133 | ``` 134 | db.default.migration.placeholderPrefix="$flyway{{{" 135 | db.default.migration.placeholderSuffix="}}}" 136 | db.default.migration.placeholders.foo="bar" 137 | db.default.migration.placeholders.hoge="pupi" 138 | ``` 139 | 140 | This would cause 141 | 142 | ```sql 143 | INSERT INTO USERS ($flyway{{{foo}}}) VALUES ('$flyway{{{hoge}}}') 144 | ``` 145 | 146 | to be rewritten to 147 | 148 | ```sql 149 | INSERT INTO USERS (bar) VALUES ('pupi') 150 | ``` 151 | 152 | ### Enable/disable Validation 153 | 154 | From flyway 3.0, `validate` run before `migrate` by default. 155 | Set `validateOnMigrate` to false if you want to disable this. 156 | 157 | ``` 158 | db.${dbName}.migration.validateOnMigrate=false // true by default 159 | ``` 160 | 161 | ### Migration prefix 162 | 163 | Custom sql migration prefix key-value pair can be specified in application.conf: 164 | 165 | ``` 166 | db.${dbName}.migration.sqlMigrationPrefix="migration_" 167 | ``` 168 | 169 | ### Dev 170 | 171 | ![screenshot](screenshot1.png) 172 | 173 | 174 | For existing schema, Flyway has a option called 'initOnMigrate'. This option is enabled when `-Ddb.${dbName}.migration.initOnMigrate=true`. 175 | For example, 176 | ``` 177 | $ play -Ddb.default.migration.initOnMigrate=true 178 | ``` 179 | 180 | Of course, You can write this in your `application.conf`. 181 | 182 | 183 | Manual migration is also supported. Click 'Other operations' or open `/@flyway/${dbName}` directly. 184 | 185 | ![screenshot](screenshot2.png) 186 | 187 | 188 | ### Test 189 | 190 | In Test mode, migration is done automatically. 191 | 192 | 193 | ### Prod 194 | 195 | In production mode, migration is done automatically if `db.${dbName}.migration.auto` is set to be true in application.conf. 196 | Otherwise it failed to start when migration is needed. 197 | 198 | ``` 199 | $ play -Ddb.default.migration.auto=true start 200 | ``` 201 | 202 | ## Example application 203 | 204 | [seratch/devteam-app](https://github.com/scalikejdbc/devteam-app "seratch/devteam-app") is using play-flyway. Maybe this is a good example. 205 | 206 | ## compile-time DI support 207 | 208 | 209 | ```scala 210 | class MyComponents(context: Context) extends BuiltInComponents(context) with FlywayPlayComponents { 211 | flywayPlayInitializer 212 | ... 213 | } 214 | ``` 215 | 216 | ## Change Log 217 | 218 | ### 3.0.1 219 | 220 | - Fix problem with locating scripts when using locations 221 | 222 | ### 3.0.0 223 | 224 | - Support Play 2.5 225 | - Support compile-time DI 226 | - Refactored view code with twirl 227 | - Ignore non-flyway db.* entry in application.conf 228 | 229 | ### 2.3.0 230 | 231 | - Flyway 4.0 232 | 233 | ### 2.2.1 234 | 235 | - Add support for Flyway sqlMigrationPrefix parameter. 236 | - Flyway 3.2.1 237 | 238 | ### 2.2.0 239 | 240 | - Removed dependency on play.api.Application 241 | 242 | ### 2.1.0 243 | 244 | - Support for specifying a list of schemas 245 | - Fixed classloader issue 246 | 247 | ### 2.0.1 248 | 249 | - Supported new configuration key, `db.default.username`. 250 | 251 | ### 2.0.0 252 | 253 | - Play 2.4 support 254 | 255 | ## License 256 | 257 | - Apache 2.0 License 258 | -------------------------------------------------------------------------------- /plugin/src/test/scala/org/flywaydb/play/ConfigReaderSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2013 Toshiyuki Takahashi 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.flywaydb.play 17 | 18 | import play.api.{ Environment, Configuration } 19 | import play.api.test._ 20 | import play.api.test.Helpers._ 21 | import org.scalatest.{ FunSpec, ShouldMatchers } 22 | 23 | class ConfigReaderSpec extends FunSpec with ShouldMatchers { 24 | 25 | val defaultDB = Map( 26 | "db.default.driver" -> "org.h2.Driver", 27 | "db.default.url" -> "jdbc:h2:mem:example;DB_CLOSE_DELAY=-1", 28 | "db.default.username" -> "sa" 29 | ) 30 | 31 | val secondaryDB = Map( 32 | "db.secondary.driver" -> "org.h2.Driver", 33 | "db.secondary.url" -> "jdbc:h2:mem:example2;DB_CLOSE_DELAY=-1", 34 | "db.secondary.username" -> "sa", 35 | "db.secondary.password" -> "secret2" 36 | ) 37 | 38 | val thirdDB = Map( 39 | "db.third.driver" -> "org.h2.Driver", 40 | "db.third.url" -> "jdbc:h2:mem:example3;DB_CLOSE_DELAY=-1", 41 | "db.third.user" -> "sa", 42 | "db.third.pass" -> "secret3" 43 | ) 44 | 45 | def withDefaultDB[A](additionalConfiguration: Map[String, Object])(assertion: FlywayConfiguration => A): A = { 46 | val configuration = Configuration((defaultDB ++ additionalConfiguration).toSeq: _*) 47 | val environment = Environment.simple() 48 | val reader = new ConfigReader(configuration, environment) 49 | val configMap = reader.getFlywayConfigurations 50 | assertion(configMap.get("default").get) 51 | } 52 | 53 | describe("ConfigReader") { 54 | 55 | it("should get database configurations") { 56 | val configuration = Configuration((defaultDB ++ secondaryDB ++ thirdDB).toSeq: _*) 57 | val environment = Environment.simple() 58 | val reader = new ConfigReader(configuration, environment) 59 | val configMap = reader.getFlywayConfigurations 60 | configMap.get("default").get.database should be(DatabaseConfiguration("org.h2.Driver", "jdbc:h2:mem:example;DB_CLOSE_DELAY=-1", "sa", null)) 61 | configMap.get("secondary").get.database should be(DatabaseConfiguration("org.h2.Driver", "jdbc:h2:mem:example2;DB_CLOSE_DELAY=-1", "sa", "secret2")) 62 | configMap.get("third").get.database should be(DatabaseConfiguration("org.h2.Driver", "jdbc:h2:mem:example3;DB_CLOSE_DELAY=-1", "sa", "secret3")) 63 | } 64 | 65 | describe("auto") { 66 | it("should be parsed") { 67 | withDefaultDB(Map("db.default.migration.auto" -> "true")) { config => 68 | config.auto should be(true) 69 | } 70 | } 71 | it("should be false by default") { 72 | withDefaultDB(Map.empty) { config => 73 | config.auto should be(false) 74 | } 75 | } 76 | } 77 | 78 | describe("initOnMigrate") { 79 | it("should be parsed") { 80 | withDefaultDB(Map("db.default.migration.initOnMigrate" -> "true")) { config => 81 | config.initOnMigrate should be(true) 82 | } 83 | } 84 | it("should be false by default") { 85 | withDefaultDB(Map.empty) { config => 86 | config.initOnMigrate should be(false) 87 | } 88 | } 89 | } 90 | 91 | describe("validateOnMigrate") { 92 | it("should be parsed") { 93 | withDefaultDB(Map("db.default.migration.validateOnMigrate" -> "false")) { config => 94 | config.validateOnMigrate should be(false) 95 | } 96 | } 97 | it("should be true by default") { 98 | withDefaultDB(Map.empty) { config => 99 | config.validateOnMigrate should be(true) 100 | } 101 | } 102 | } 103 | 104 | describe("encoding") { 105 | it("should be parsed") { 106 | withDefaultDB(Map("db.default.migration.encoding" -> "EUC-JP")) { config => 107 | config.encoding should be("EUC-JP") 108 | } 109 | } 110 | it("should be UTF-8 by default") { 111 | withDefaultDB(Map.empty) { config => 112 | config.encoding should be("UTF-8") 113 | } 114 | } 115 | } 116 | 117 | describe("placeholderPrefix") { 118 | it("should be parsed") { 119 | withDefaultDB(Map("db.default.migration.placeholderPrefix" -> "PREFIX_")) { config => 120 | config.placeholderPrefix should be(Some("PREFIX_")) 121 | } 122 | } 123 | it("should be None by default") { 124 | withDefaultDB(Map.empty) { config => 125 | config.placeholderPrefix should be(None) 126 | } 127 | } 128 | } 129 | 130 | describe("placeholderSuffix") { 131 | it("should be parsed") { 132 | withDefaultDB(Map("db.default.migration.placeholderSuffix" -> "SUFFIX_")) { config => 133 | config.placeholderSuffix should be(Some("SUFFIX_")) 134 | } 135 | } 136 | it("should be None by default") { 137 | withDefaultDB(Map.empty) { config => 138 | config.placeholderSuffix should be(None) 139 | } 140 | } 141 | } 142 | 143 | describe("placeholder") { 144 | it("should be parsed") { 145 | withDefaultDB(Map( 146 | "db.default.migration.placeholders.fleetwood" -> "mac", 147 | "db.default.migration.placeholders.buckingham" -> "nicks" 148 | )) { config => 149 | config.placeholders should be( 150 | Map( 151 | "fleetwood" -> "mac", 152 | "buckingham" -> "nicks" 153 | )) 154 | } 155 | } 156 | it("should be empty by default") { 157 | withDefaultDB(Map.empty) { config => 158 | config.placeholders should be('empty) 159 | } 160 | } 161 | } 162 | 163 | describe("outOfOrder") { 164 | it("should be parsed") { 165 | withDefaultDB(Map("db.default.migration.outOfOrder" -> "true")) { config => 166 | config.outOfOrder should be(true) 167 | } 168 | } 169 | it("should be false by default") { 170 | withDefaultDB(Map.empty) { config => 171 | config.outOfOrder should be(false) 172 | } 173 | } 174 | } 175 | 176 | describe("schemas") { 177 | it("should be parsed") { 178 | withDefaultDB(Map("db.default.migration.schemas" -> List("public", "other"))) { config => 179 | config.schemas should be(List("public", "other")) 180 | } 181 | } 182 | it("should be None by default") { 183 | withDefaultDB(Map.empty) { config => 184 | config.schemas should be(List.empty) 185 | } 186 | } 187 | } 188 | 189 | describe("locations") { 190 | it("should be parsed") { 191 | withDefaultDB(Map("db.default.migration.locations" -> List("h2", "common"))) { config => 192 | config.locations should be(List("h2", "common")) 193 | } 194 | } 195 | it("should be None by default") { 196 | withDefaultDB(Map.empty) { config => 197 | config.locations should be(List.empty) 198 | } 199 | } 200 | } 201 | 202 | describe("sqlMigrationPrefix") { 203 | it("should be parsed") { 204 | withDefaultDB(Map("db.default.migration.sqlMigrationPrefix" -> "migration_")) { config => 205 | config.sqlMigrationPrefix should be(Some("migration_")) 206 | } 207 | } 208 | it("should be None by default") { 209 | withDefaultDB(Map.empty) { config => 210 | config.sqlMigrationPrefix should be(None) 211 | } 212 | } 213 | } 214 | 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /playapp/public/javascripts/jquery-1.9.0.min.js: -------------------------------------------------------------------------------- 1 | /*! jQuery v1.9.0 | (c) 2005, 2012 jQuery Foundation, Inc. | jquery.org/license */(function(e,t){"use strict";function n(e){var t=e.length,n=st.type(e);return st.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||"function"!==n&&(0===t||"number"==typeof t&&t>0&&t-1 in e)}function r(e){var t=Tt[e]={};return st.each(e.match(lt)||[],function(e,n){t[n]=!0}),t}function i(e,n,r,i){if(st.acceptData(e)){var o,a,s=st.expando,u="string"==typeof n,l=e.nodeType,c=l?st.cache:e,f=l?e[s]:e[s]&&s;if(f&&c[f]&&(i||c[f].data)||!u||r!==t)return f||(l?e[s]=f=K.pop()||st.guid++:f=s),c[f]||(c[f]={},l||(c[f].toJSON=st.noop)),("object"==typeof n||"function"==typeof n)&&(i?c[f]=st.extend(c[f],n):c[f].data=st.extend(c[f].data,n)),o=c[f],i||(o.data||(o.data={}),o=o.data),r!==t&&(o[st.camelCase(n)]=r),u?(a=o[n],null==a&&(a=o[st.camelCase(n)])):a=o,a}}function o(e,t,n){if(st.acceptData(e)){var r,i,o,a=e.nodeType,u=a?st.cache:e,l=a?e[st.expando]:st.expando;if(u[l]){if(t&&(r=n?u[l]:u[l].data)){st.isArray(t)?t=t.concat(st.map(t,st.camelCase)):t in r?t=[t]:(t=st.camelCase(t),t=t in r?[t]:t.split(" "));for(i=0,o=t.length;o>i;i++)delete r[t[i]];if(!(n?s:st.isEmptyObject)(r))return}(n||(delete u[l].data,s(u[l])))&&(a?st.cleanData([e],!0):st.support.deleteExpando||u!=u.window?delete u[l]:u[l]=null)}}}function a(e,n,r){if(r===t&&1===e.nodeType){var i="data-"+n.replace(Nt,"-$1").toLowerCase();if(r=e.getAttribute(i),"string"==typeof r){try{r="true"===r?!0:"false"===r?!1:"null"===r?null:+r+""===r?+r:wt.test(r)?st.parseJSON(r):r}catch(o){}st.data(e,n,r)}else r=t}return r}function s(e){var t;for(t in e)if(("data"!==t||!st.isEmptyObject(e[t]))&&"toJSON"!==t)return!1;return!0}function u(){return!0}function l(){return!1}function c(e,t){do e=e[t];while(e&&1!==e.nodeType);return e}function f(e,t,n){if(t=t||0,st.isFunction(t))return st.grep(e,function(e,r){var i=!!t.call(e,r,e);return i===n});if(t.nodeType)return st.grep(e,function(e){return e===t===n});if("string"==typeof t){var r=st.grep(e,function(e){return 1===e.nodeType});if(Wt.test(t))return st.filter(t,r,!n);t=st.filter(t,r)}return st.grep(e,function(e){return st.inArray(e,t)>=0===n})}function p(e){var t=zt.split("|"),n=e.createDocumentFragment();if(n.createElement)for(;t.length;)n.createElement(t.pop());return n}function d(e,t){return e.getElementsByTagName(t)[0]||e.appendChild(e.ownerDocument.createElement(t))}function h(e){var t=e.getAttributeNode("type");return e.type=(t&&t.specified)+"/"+e.type,e}function g(e){var t=nn.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function m(e,t){for(var n,r=0;null!=(n=e[r]);r++)st._data(n,"globalEval",!t||st._data(t[r],"globalEval"))}function y(e,t){if(1===t.nodeType&&st.hasData(e)){var n,r,i,o=st._data(e),a=st._data(t,o),s=o.events;if(s){delete a.handle,a.events={};for(n in s)for(r=0,i=s[n].length;i>r;r++)st.event.add(t,n,s[n][r])}a.data&&(a.data=st.extend({},a.data))}}function v(e,t){var n,r,i;if(1===t.nodeType){if(n=t.nodeName.toLowerCase(),!st.support.noCloneEvent&&t[st.expando]){r=st._data(t);for(i in r.events)st.removeEvent(t,i,r.handle);t.removeAttribute(st.expando)}"script"===n&&t.text!==e.text?(h(t).text=e.text,g(t)):"object"===n?(t.parentNode&&(t.outerHTML=e.outerHTML),st.support.html5Clone&&e.innerHTML&&!st.trim(t.innerHTML)&&(t.innerHTML=e.innerHTML)):"input"===n&&Zt.test(e.type)?(t.defaultChecked=t.checked=e.checked,t.value!==e.value&&(t.value=e.value)):"option"===n?t.defaultSelected=t.selected=e.defaultSelected:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}}function b(e,n){var r,i,o=0,a=e.getElementsByTagName!==t?e.getElementsByTagName(n||"*"):e.querySelectorAll!==t?e.querySelectorAll(n||"*"):t;if(!a)for(a=[],r=e.childNodes||e;null!=(i=r[o]);o++)!n||st.nodeName(i,n)?a.push(i):st.merge(a,b(i,n));return n===t||n&&st.nodeName(e,n)?st.merge([e],a):a}function x(e){Zt.test(e.type)&&(e.defaultChecked=e.checked)}function T(e,t){if(t in e)return t;for(var n=t.charAt(0).toUpperCase()+t.slice(1),r=t,i=Nn.length;i--;)if(t=Nn[i]+n,t in e)return t;return r}function w(e,t){return e=t||e,"none"===st.css(e,"display")||!st.contains(e.ownerDocument,e)}function N(e,t){for(var n,r=[],i=0,o=e.length;o>i;i++)n=e[i],n.style&&(r[i]=st._data(n,"olddisplay"),t?(r[i]||"none"!==n.style.display||(n.style.display=""),""===n.style.display&&w(n)&&(r[i]=st._data(n,"olddisplay",S(n.nodeName)))):r[i]||w(n)||st._data(n,"olddisplay",st.css(n,"display")));for(i=0;o>i;i++)n=e[i],n.style&&(t&&"none"!==n.style.display&&""!==n.style.display||(n.style.display=t?r[i]||"":"none"));return e}function C(e,t,n){var r=mn.exec(t);return r?Math.max(0,r[1]-(n||0))+(r[2]||"px"):t}function k(e,t,n,r,i){for(var o=n===(r?"border":"content")?4:"width"===t?1:0,a=0;4>o;o+=2)"margin"===n&&(a+=st.css(e,n+wn[o],!0,i)),r?("content"===n&&(a-=st.css(e,"padding"+wn[o],!0,i)),"margin"!==n&&(a-=st.css(e,"border"+wn[o]+"Width",!0,i))):(a+=st.css(e,"padding"+wn[o],!0,i),"padding"!==n&&(a+=st.css(e,"border"+wn[o]+"Width",!0,i)));return a}function E(e,t,n){var r=!0,i="width"===t?e.offsetWidth:e.offsetHeight,o=ln(e),a=st.support.boxSizing&&"border-box"===st.css(e,"boxSizing",!1,o);if(0>=i||null==i){if(i=un(e,t,o),(0>i||null==i)&&(i=e.style[t]),yn.test(i))return i;r=a&&(st.support.boxSizingReliable||i===e.style[t]),i=parseFloat(i)||0}return i+k(e,t,n||(a?"border":"content"),r,o)+"px"}function S(e){var t=V,n=bn[e];return n||(n=A(e,t),"none"!==n&&n||(cn=(cn||st("