diff --git a/etc/deploy-assemby-jar.sh b/etc/deploy-assemby-jar.sh
new file mode 100644
index 000000000..11f276d1d
--- /dev/null
+++ b/etc/deploy-assemby-jar.sh
@@ -0,0 +1,9 @@
+#!/bin/sh
+mvn deploy:deploy-file \
+ -DgroupId=jp.sf.amateras\
+ -DartifactId=gitbucket-assembly\
+ -Dversion=0.0.1\
+ -Dpackaging=jar\
+ -Dfile=../target/scala-2.11/gitbucket-assembly-0.0.1.jar\
+ -DrepositoryId=sourceforge.jp\
+ -Durl=scp://shell.sourceforge.jp/home/groups/a/am/amateras/htdocs/mvn/
\ No newline at end of file
diff --git a/etc/pom.xml b/etc/pom.xml
new file mode 100644
index 000000000..40693f237
--- /dev/null
+++ b/etc/pom.xml
@@ -0,0 +1,17 @@
+
+
+ 4.0.0
+ jp.sf.amateras
+ gitbucket-assembly
+ 0.0.1
+
+
+
+ org.apache.maven.wagon
+ wagon-ssh
+ 1.0-beta-6
+
+
+
+
\ No newline at end of file
diff --git a/project/build.scala b/project/build.scala
index e76707879..7d4153809 100644
--- a/project/build.scala
+++ b/project/build.scala
@@ -4,6 +4,8 @@ import org.scalatra.sbt._
import com.typesafe.sbteclipse.plugin.EclipsePlugin.EclipseKeys
import play.twirl.sbt.SbtTwirl
import play.twirl.sbt.Import.TwirlKeys._
+import sbtassembly._
+import sbtassembly.AssemblyKeys._
object MyBuild extends Build {
val Organization = "jp.sf.amateras"
@@ -17,6 +19,17 @@ object MyBuild extends Build {
file(".")
)
.settings(ScalatraPlugin.scalatraWithJRebel: _*)
+ .settings(
+ test in assembly := {},
+ assemblyMergeStrategy in assembly := {
+ case PathList("META-INF", xs @ _*) =>
+ (xs map {_.toLowerCase}) match {
+ case ("manifest.mf" :: Nil) => MergeStrategy.discard
+ case _ => MergeStrategy.discard
+ }
+ case x => MergeStrategy.first
+ }
+ )
.settings(
sourcesInBase := false,
organization := Organization,
@@ -45,7 +58,7 @@ object MyBuild extends Build {
"com.typesafe.slick" %% "slick" % "2.1.0",
"com.novell.ldap" % "jldap" % "2009-10-07",
"com.h2database" % "h2" % "1.4.180",
- "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
+// "ch.qos.logback" % "logback-classic" % "1.0.13" % "runtime",
"org.eclipse.jetty" % "jetty-webapp" % "8.1.8.v20121106" % "container;provided",
"org.eclipse.jetty.orbit" % "javax.servlet" % "3.0.0.v201112011016" % "container;provided;test" artifacts Artifact("javax.servlet", "jar", "jar"),
"junit" % "junit" % "4.11" % "test",
diff --git a/project/plugins.sbt b/project/plugins.sbt
index de95ab6af..dee3cdf87 100644
--- a/project/plugins.sbt
+++ b/project/plugins.sbt
@@ -7,3 +7,5 @@ addSbtPlugin("org.scalatra.sbt" % "scalatra-sbt" % "0.3.5")
addSbtPlugin("com.typesafe.sbt" % "sbt-twirl" % "1.0.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.1.4")
+
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.12.0")
\ No newline at end of file
diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala
index bdbd89867..cfa37e62d 100644
--- a/src/main/scala/ScalatraBootstrap.scala
+++ b/src/main/scala/ScalatraBootstrap.scala
@@ -1,5 +1,7 @@
import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter}
import app._
+import plugin.PluginRegistry
+
//import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider
import org.scalatra._
import javax.servlet._
@@ -15,6 +17,11 @@ class ScalatraBootstrap extends LifeCycle {
// Register controllers
context.mount(new AnonymousAccessController, "/*")
+
+ PluginRegistry().getControllers.foreach { case (controller, path) =>
+ context.mount(controller, path)
+ }
+
context.mount(new IndexController, "/")
context.mount(new SearchController, "/")
context.mount(new FileUploadController, "/upload")
diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala
new file mode 100644
index 000000000..c17687466
--- /dev/null
+++ b/src/main/scala/plugin/Plugin.scala
@@ -0,0 +1,30 @@
+package plugin
+
+import javax.servlet.ServletContext
+
+import util.Version
+
+/**
+ * Trait for define plugin interface.
+ * To provide plugin, put Plugin class which mixed in this trait into the package root.
+ */
+trait Plugin {
+
+ val pluginId: String
+ val pluginName: String
+ val description: String
+ val versions: Seq[Version]
+
+ /**
+ * This method is invoked in initialization of plugin system.
+ * Register plugin functionality to PluginRegistry.
+ */
+ def initialize(registry: PluginRegistry): Unit
+
+ /**
+ * This method is invoked in shutdown of plugin system.
+ * If the plugin has any resources, release them in this method.
+ */
+ def shutdown(registry: PluginRegistry): Unit
+
+}
diff --git a/src/main/scala/plugin/PluginRegistory.scala b/src/main/scala/plugin/PluginRegistory.scala
new file mode 100644
index 000000000..bb7c2cb3b
--- /dev/null
+++ b/src/main/scala/plugin/PluginRegistory.scala
@@ -0,0 +1,145 @@
+package plugin
+
+import java.io.{FilenameFilter, File}
+import java.net.URLClassLoader
+import javax.servlet.ServletContext
+import javax.servlet.http.{HttpServletRequest, HttpServletResponse}
+
+import org.slf4j.LoggerFactory
+import service.RepositoryService.RepositoryInfo
+import util.Directory._
+import util.JDBCUtil._
+import util.{Version, Versions}
+
+import scala.collection.mutable.ListBuffer
+import app.{ControllerBase, Context}
+
+class PluginRegistry {
+
+ private val plugins = new ListBuffer[PluginInfo]
+ private val javaScripts = new ListBuffer[(String, String)]
+ private val controllers = new ListBuffer[(ControllerBase, String)]
+
+ def addPlugin(pluginInfo: PluginInfo): Unit = {
+ plugins += pluginInfo
+ }
+
+ def getPlugins(): List[PluginInfo] = plugins.toList
+
+ def addController(controller: ControllerBase, path: String): Unit = {
+ controllers += ((controller, path))
+ }
+
+ def getControllers(): List[(ControllerBase, String)] = controllers.toList
+
+ def addJavaScript(path: String, script: String): Unit = {
+ javaScripts += Tuple2(path, script)
+ }
+
+ //def getJavaScripts(): List[(String, String)] = javaScripts.toList
+
+ def getJavaScript(currentPath: String): Option[String] = {
+ javaScripts.find(x => currentPath.matches(x._1)).map(_._2)
+ }
+
+ private case class GlobalAction(
+ method: String,
+ path: String,
+ function: (HttpServletRequest, HttpServletResponse, Context) => Any
+ )
+
+ private case class RepositoryAction(
+ method: String,
+ path: String,
+ function: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any
+ )
+
+}
+
+/**
+ * Provides entry point to PluginRegistry.
+ */
+object PluginRegistry {
+
+ private val logger = LoggerFactory.getLogger(classOf[PluginRegistry])
+
+ private val instance = new PluginRegistry()
+
+ /**
+ * Returns the PluginRegistry singleton instance.
+ */
+ def apply(): PluginRegistry = instance
+
+ /**
+ * Initializes all installed plugins.
+ */
+ def initialize(context: ServletContext, conn: java.sql.Connection): Unit = {
+ val pluginDir = new File(PluginHome)
+ if(pluginDir.exists && pluginDir.isDirectory){
+ pluginDir.listFiles(new FilenameFilter {
+ override def accept(dir: File, name: String): Boolean = name.endsWith(".jar")
+ }).foreach { pluginJar =>
+ val classLoader = new URLClassLoader(Array(pluginJar.toURI.toURL), Thread.currentThread.getContextClassLoader)
+ try {
+ val plugin = classLoader.loadClass("Plugin").newInstance().asInstanceOf[Plugin]
+
+ // Migration
+ val headVersion = plugin.versions.head
+ val currentVersion = conn.find("SELECT * FROM PLUGIN WHERE PLUGIN_ID = ?", plugin.pluginId)(_.getString("VERSION")) match {
+ case Some(x) => {
+ val dim = x.split("\\.")
+ Version(dim(0).toInt, dim(1).toInt)
+ }
+ case None => Version(0, 0)
+ }
+
+ Versions.update(conn, headVersion, currentVersion, plugin.versions, new URLClassLoader(Array(pluginJar.toURI.toURL))){ conn =>
+ currentVersion.versionString match {
+ case "0.0" =>
+ conn.update("INSERT INTO PLUGIN (PLUGIN_ID, VERSION) VALUES (?, ?)", plugin.pluginId, headVersion.versionString)
+ case _ =>
+ conn.update("UPDATE PLUGIN SET VERSION = ? WHERE PLUGIN_ID = ?", headVersion.versionString, plugin.pluginId)
+ }
+ }
+
+ // Initialize
+ plugin.initialize(instance)
+ instance.addPlugin(PluginInfo(
+ pluginId = plugin.pluginId,
+ pluginName = plugin.pluginName,
+ version = plugin.versions.head.versionString,
+ description = plugin.description,
+ pluginClass = plugin
+ ))
+
+ } catch {
+ case e: Exception => {
+ logger.error(s"Error during plugin initialization", e)
+ }
+ }
+ }
+ }
+ }
+
+ def shutdown(context: ServletContext): Unit = {
+ instance.getPlugins().foreach { pluginInfo =>
+ try {
+ pluginInfo.pluginClass.shutdown(instance)
+ } catch {
+ case e: Exception => {
+ logger.error(s"Error during plugin shutdown", e)
+ }
+ }
+ }
+ }
+
+
+}
+
+case class PluginInfo(
+ pluginId: String,
+ pluginName: String,
+ version: String,
+ description: String,
+ pluginClass: Plugin
+)
\ No newline at end of file
diff --git a/src/main/scala/plugin/Results.scala b/src/main/scala/plugin/Results.scala
new file mode 100644
index 000000000..18fdb7fc4
--- /dev/null
+++ b/src/main/scala/plugin/Results.scala
@@ -0,0 +1,11 @@
+package plugin
+
+import play.twirl.api.Html
+
+/**
+ * Defines result case classes returned by plugin controller.
+ */
+object Results {
+ case class Redirect(path: String)
+ case class Fragment(html: Html)
+}
diff --git a/src/main/scala/plugin/Sessions.scala b/src/main/scala/plugin/Sessions.scala
new file mode 100644
index 000000000..7398c9a8d
--- /dev/null
+++ b/src/main/scala/plugin/Sessions.scala
@@ -0,0 +1,11 @@
+package plugin
+
+import slick.jdbc.JdbcBackend.Session
+
+/**
+ * Provides Slick Session to Plug-ins.
+ */
+object Sessions {
+ val sessions = new ThreadLocal[Session]
+ implicit def session: Session = sessions.get()
+}
diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/InitializeListener.scala
similarity index 69%
rename from src/main/scala/servlet/AutoUpdateListener.scala
rename to src/main/scala/servlet/InitializeListener.scala
index 30610bcb9..fdc3c48bf 100644
--- a/src/main/scala/servlet/AutoUpdateListener.scala
+++ b/src/main/scala/servlet/InitializeListener.scala
@@ -4,49 +4,16 @@ import java.io.File
import java.sql.{DriverManager, Connection}
import org.apache.commons.io.FileUtils
import javax.servlet.{ServletContextListener, ServletContextEvent}
-import org.apache.commons.io.IOUtils
import org.slf4j.LoggerFactory
import util.Directory._
import util.ControlUtil._
import util.JDBCUtil._
import org.eclipse.jgit.api.Git
+import util.{Version, Versions}
+import plugin._
import util.{DatabaseConfig, Directory}
object AutoUpdate {
-
- /**
- * Version of GitBucket
- *
- * @param majorVersion the major version
- * @param minorVersion the minor version
- */
- case class Version(majorVersion: Int, minorVersion: Int){
-
- private val logger = LoggerFactory.getLogger(classOf[servlet.AutoUpdate.Version])
-
- /**
- * Execute update/MAJOR_MINOR.sql to update schema to this version.
- * If corresponding SQL file does not exist, this method do nothing.
- */
- def update(conn: Connection): Unit = {
- val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
-
- using(Thread.currentThread.getContextClassLoader.getResourceAsStream(sqlPath)){ in =>
- if(in != null){
- val sql = IOUtils.toString(in, "UTF-8")
- using(conn.createStatement()){ stmt =>
- logger.debug(sqlPath + "=" + sql)
- stmt.executeUpdate(sql)
- }
- }
- }
- }
-
- /**
- * MAJOR.MINOR
- */
- val versionString = s"${majorVersion}.${minorVersion}"
- }
/**
* The history of versions. A head of this sequence is the current BitBucket version.
@@ -54,8 +21,8 @@ object AutoUpdate {
val versions = Seq(
new Version(2, 8),
new Version(2, 7) {
- override def update(conn: Connection): Unit = {
- super.update(conn)
+ override def update(conn: Connection, cl: ClassLoader): Unit = {
+ super.update(conn, cl)
conn.select("SELECT * FROM REPOSITORY"){ rs =>
// Rename attached files directory from /issues to /comments
val userName = rs.getString("USER_NAME")
@@ -93,8 +60,8 @@ object AutoUpdate {
new Version(2, 5),
new Version(2, 4),
new Version(2, 3) {
- override def update(conn: Connection): Unit = {
- super.update(conn)
+ override def update(conn: Connection, cl: ClassLoader): Unit = {
+ super.update(conn, cl)
conn.select("SELECT ACTIVITY_ID, ADDITIONAL_INFO FROM ACTIVITY WHERE ACTIVITY_TYPE='push'"){ rs =>
val curInfo = rs.getString("ADDITIONAL_INFO")
val newInfo = curInfo.split("\n").filter(_ matches "^[0-9a-z]{40}:.*").mkString("\n")
@@ -102,20 +69,22 @@ object AutoUpdate {
conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID"))
}
}
- FileUtils.deleteDirectory(Directory.getPluginCacheDir())
- FileUtils.deleteDirectory(new File(Directory.PluginHome))
+ ignore {
+ FileUtils.deleteDirectory(Directory.getPluginCacheDir())
+ //FileUtils.deleteDirectory(new File(Directory.PluginHome))
+ }
}
},
new Version(2, 2),
new Version(2, 1),
new Version(2, 0){
- override def update(conn: Connection): Unit = {
+ override def update(conn: Connection, cl: ClassLoader): Unit = {
import eu.medsea.mimeutil.{MimeUtil2, MimeType}
val mimeUtil = new MimeUtil2()
mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector")
- super.update(conn)
+ super.update(conn, cl)
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
defining(Directory.getAttachedDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME"))){ dir =>
if(dir.exists && dir.isDirectory){
@@ -143,8 +112,8 @@ object AutoUpdate {
Version(1, 5),
Version(1, 4),
new Version(1, 3){
- override def update(conn: Connection): Unit = {
- super.update(conn)
+ override def update(conn: Connection, cl: ClassLoader): Unit = {
+ super.update(conn, cl)
// Fix wiki repository configuration
conn.select("SELECT USER_NAME, REPOSITORY_NAME FROM REPOSITORY"){ rs =>
using(Git.open(getWikiRepositoryDir(rs.getString("USER_NAME"), rs.getString("REPOSITORY_NAME")))){ git =>
@@ -193,14 +162,14 @@ object AutoUpdate {
}
/**
- * Update database schema automatically in the context initializing.
+ * Initialize GitBucket system.
+ * Update database schema and load plug-ins automatically in the context initializing.
*/
-class AutoUpdateListener extends ServletContextListener {
+class InitializeListener extends ServletContextListener {
import AutoUpdate._
- private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener])
-// private val scheduler = StdSchedulerFactory.getDefaultScheduler
-
+ private val logger = LoggerFactory.getLogger(classOf[InitializeListener])
+
override def contextInitialized(event: ServletContextEvent): Unit = {
val dataDir = event.getServletContext.getInitParameter("gitbucket.home")
if(dataDir != null){
@@ -209,31 +178,21 @@ class AutoUpdateListener extends ServletContextListener {
org.h2.Driver.load()
defining(getConnection()){ conn =>
+ // Migration
logger.debug("Start schema update")
- try {
- defining(getCurrentVersion()){ currentVersion =>
- if(currentVersion == headVersion){
- logger.debug("No update")
- } else if(!versions.contains(currentVersion)){
- logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
- } else {
- versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn))
- FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
- logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
- }
- }
- } catch {
- case ex: Throwable => {
- logger.error("Failed to schema update", ex)
- ex.printStackTrace()
- conn.rollback()
- }
+ Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn =>
+ FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8")
}
- logger.debug("End schema update")
+ // Load plugins
+ logger.debug("Initialize plugins")
+ PluginRegistry.initialize(event.getServletContext, conn)
}
+
}
- def contextDestroyed(sce: ServletContextEvent): Unit = {
+ def contextDestroyed(event: ServletContextEvent): Unit = {
+ // Shutdown plugins
+ PluginRegistry.shutdown(event.getServletContext)
}
private def getConnection(): Connection =
diff --git a/src/main/scala/util/ControlUtil.scala b/src/main/scala/util/ControlUtil.scala
index c231fb01f..7945f32a5 100644
--- a/src/main/scala/util/ControlUtil.scala
+++ b/src/main/scala/util/ControlUtil.scala
@@ -37,4 +37,10 @@ object ControlUtil {
def using[T](treeWalk: TreeWalk)(f: TreeWalk => T): T =
try f(treeWalk) finally treeWalk.release()
+ def ignore[T](f: => Unit): Unit = try {
+ f
+ } catch {
+ case e: Exception => ()
+ }
+
}
diff --git a/src/main/scala/util/JDBCUtil.scala b/src/main/scala/util/JDBCUtil.scala
index 375ea1464..5d880d0c5 100644
--- a/src/main/scala/util/JDBCUtil.scala
+++ b/src/main/scala/util/JDBCUtil.scala
@@ -18,6 +18,14 @@ object JDBCUtil {
}
}
+ def find[T](sql: String, params: Any*)(f: ResultSet => T): Option[T] = {
+ execute(sql, params: _*){ stmt =>
+ using(stmt.executeQuery()){ rs =>
+ if(rs.next) Some(f(rs)) else None
+ }
+ }
+ }
+
def select[T](sql: String, params: Any*)(f: ResultSet => T): Seq[T] = {
execute(sql, params: _*){ stmt =>
using(stmt.executeQuery()){ rs =>
diff --git a/src/main/scala/util/Version.scala b/src/main/scala/util/Version.scala
new file mode 100644
index 000000000..fed74a8e0
--- /dev/null
+++ b/src/main/scala/util/Version.scala
@@ -0,0 +1,67 @@
+package util
+
+import java.sql.Connection
+
+import org.apache.commons.io.IOUtils
+import org.slf4j.LoggerFactory
+import util.ControlUtil._
+
+case class Version(majorVersion: Int, minorVersion: Int) {
+
+ private val logger = LoggerFactory.getLogger(classOf[Version])
+
+ /**
+ * Execute update/MAJOR_MINOR.sql to update schema to this version.
+ * If corresponding SQL file does not exist, this method do nothing.
+ */
+ def update(conn: Connection, cl: ClassLoader): Unit = {
+ val sqlPath = s"update/${majorVersion}_${minorVersion}.sql"
+
+ using(cl.getResourceAsStream(sqlPath)){ in =>
+ if(in != null){
+ val sql = IOUtils.toString(in, "UTF-8")
+ using(conn.createStatement()){ stmt =>
+ logger.debug(sqlPath + "=" + sql)
+ stmt.executeUpdate(sql)
+ }
+ }
+ }
+ }
+
+
+ /**
+ * MAJOR.MINOR
+ */
+ val versionString = s"${majorVersion}.${minorVersion}"
+
+}
+
+object Versions {
+
+ private val logger = LoggerFactory.getLogger(Versions.getClass)
+
+ def update(conn: Connection, headVersion: Version, currentVersion: Version, versions: Seq[Version], cl: ClassLoader)
+ (save: Connection => Unit): Unit = {
+ logger.debug("Start schema update")
+ try {
+ if(currentVersion == headVersion){
+ logger.debug("No update")
+ } else if(currentVersion.versionString != "0.0" && !versions.contains(currentVersion)){
+ logger.warn(s"Skip migration because ${currentVersion.versionString} is illegal version.")
+ } else {
+ versions.takeWhile(_ != currentVersion).reverse.foreach(_.update(conn, cl))
+ save(conn)
+ logger.debug(s"Updated from ${currentVersion.versionString} to ${headVersion.versionString}")
+ }
+ } catch {
+ case ex: Throwable => {
+ logger.error("Failed to schema update", ex)
+ ex.printStackTrace()
+ conn.rollback()
+ }
+ }
+ logger.debug("End schema update")
+ }
+
+}
+
diff --git a/src/main/twirl/main.scala.html b/src/main/twirl/main.scala.html
index 9b8ae0280..6d35ff1d1 100644
--- a/src/main/twirl/main.scala.html
+++ b/src/main/twirl/main.scala.html
@@ -82,5 +82,10 @@
});
});
+ @plugin.PluginRegistry().getJavaScript(request.getRequestURI).map { script =>
+
+ }