diff --git a/src/main/scala/ScalatraBootstrap.scala b/src/main/scala/ScalatraBootstrap.scala index aa90776..c2dfb3e 100644 --- a/src/main/scala/ScalatraBootstrap.scala +++ b/src/main/scala/ScalatraBootstrap.scala @@ -1,5 +1,7 @@ -import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter, PluginActionFilter} +import _root_.servlet.{BasicAuthenticationFilter, TransactionFilter} import app._ +import plugin.PluginRegistry + //import jp.sf.amateras.scalatra.forms.ValidationJavaScriptProvider import org.scalatra._ import javax.servlet._ @@ -12,8 +14,6 @@ context.getFilterRegistration("transactionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") context.addFilter("basicAuthenticationFilter", new BasicAuthenticationFilter) context.getFilterRegistration("basicAuthenticationFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/git/*") - context.addFilter("pluginActionFilter", new PluginActionFilter) - context.getFilterRegistration("pluginActionFilter").addMappingForUrlPatterns(EnumSet.allOf(classOf[DispatcherType]), true, "/*") // Register controllers context.mount(new AnonymousAccessController, "/*") @@ -24,6 +24,11 @@ context.mount(new UserManagementController, "/*") context.mount(new SystemSettingsController, "/*") context.mount(new AccountController, "/*") + + PluginRegistry().getControllers.foreach { case (controller, path) => + context.mount(controller, path) + } + context.mount(new RepositoryViewerController, "/*") context.mount(new WikiController, "/*") context.mount(new LabelsController, "/*") diff --git a/src/main/scala/plugin/Plugin.scala b/src/main/scala/plugin/Plugin.scala index e797699..c176874 100644 --- a/src/main/scala/plugin/Plugin.scala +++ b/src/main/scala/plugin/Plugin.scala @@ -1,5 +1,7 @@ package plugin +import javax.servlet.ServletContext + import util.Version /** diff --git a/src/main/scala/plugin/PluginRegistory.scala b/src/main/scala/plugin/PluginRegistory.scala index c924922..bb7c2cb 100644 --- a/src/main/scala/plugin/PluginRegistory.scala +++ b/src/main/scala/plugin/PluginRegistory.scala @@ -2,6 +2,7 @@ import java.io.{FilenameFilter, File} import java.net.URLClassLoader +import javax.servlet.ServletContext import javax.servlet.http.{HttpServletRequest, HttpServletResponse} import org.slf4j.LoggerFactory @@ -11,14 +12,13 @@ import util.{Version, Versions} import scala.collection.mutable.ListBuffer -import app.Context +import app.{ControllerBase, Context} class PluginRegistry { private val plugins = new ListBuffer[PluginInfo] private val javaScripts = new ListBuffer[(String, String)] - private val globalActions = new ListBuffer[GlobalAction] - private val repositoryActions = new ListBuffer[RepositoryAction] + private val controllers = new ListBuffer[(ControllerBase, String)] def addPlugin(pluginInfo: PluginInfo): Unit = { plugins += pluginInfo @@ -26,28 +26,11 @@ def getPlugins(): List[PluginInfo] = plugins.toList - def addGlobalAction(method: String, path: String)(f: (HttpServletRequest, HttpServletResponse, Context) => Any): Unit = { - globalActions += GlobalAction(method.toLowerCase, path, f) + def addController(controller: ControllerBase, path: String): Unit = { + controllers += ((controller, path)) } - //def getGlobalActions(): List[GlobalAction] = globalActions.toList - - def getGlobalAction(method: String, path: String): Option[(HttpServletRequest, HttpServletResponse, Context) => Any] = { - globalActions.find { globalAction => - globalAction.method == method.toLowerCase && path.matches(globalAction.path) - }.map(_.function) - } - - def addRepositoryAction(method: String, path: String)(f: (HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any): Unit = { - repositoryActions += RepositoryAction(method.toLowerCase, path, f) - } - - //def getRepositoryActions(): List[RepositoryAction] = repositoryActions.toList - - def getRepositoryAction(method: String, path: String): Option[(HttpServletRequest, HttpServletResponse, Context, RepositoryInfo) => Any] = { - // TODO - null - } + def getControllers(): List[(ControllerBase, String)] = controllers.toList def addJavaScript(path: String, script: String): Unit = { javaScripts += Tuple2(path, script) @@ -90,7 +73,7 @@ /** * Initializes all installed plugins. */ - def initialize(conn: java.sql.Connection): Unit = { + def initialize(context: ServletContext, conn: java.sql.Connection): Unit = { val pluginDir = new File(PluginHome) if(pluginDir.exists && pluginDir.isDirectory){ pluginDir.listFiles(new FilenameFilter { @@ -138,7 +121,7 @@ } } - def shutdown(): Unit = { + def shutdown(context: ServletContext): Unit = { instance.getPlugins().foreach { pluginInfo => try { pluginInfo.pluginClass.shutdown(instance) diff --git a/src/main/scala/servlet/AutoUpdateListener.scala b/src/main/scala/servlet/AutoUpdateListener.scala deleted file mode 100644 index 2c198ba..0000000 --- a/src/main/scala/servlet/AutoUpdateListener.scala +++ /dev/null @@ -1,203 +0,0 @@ -package servlet - -import java.io.File -import java.sql.{DriverManager, Connection} -import org.apache.commons.io.FileUtils -import javax.servlet.{ServletContextListener, ServletContextEvent} -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 { - - /** - * The history of versions. A head of this sequence is the current BitBucket version. - */ - val versions = Seq( - new Version(2, 8), - new Version(2, 7) { - 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") - val repoName = rs.getString("REPOSITORY_NAME") - defining(Directory.getAttachedDir(userName, repoName)){ newDir => - val oldDir = new File(newDir.getParentFile, "issues") - if(oldDir.exists && oldDir.isDirectory){ - oldDir.renameTo(newDir) - } - } - // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist - val originalUserName = rs.getString("ORIGIN_USER_NAME") - val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") - if(originalUserName != null && originalRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - originalUserName, originalRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist - val parentUserName = rs.getString("PARENT_USER_NAME") - val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") - if(parentUserName != null && parentRepoName != null){ - if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", - parentUserName, parentRepoName) == 0){ - conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + - "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) - } - } - } - } - }, - new Version(2, 6), - new Version(2, 5), - new Version(2, 4), - new Version(2, 3) { - 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") - if (curInfo != newInfo) { - conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) - } - } - 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, cl: ClassLoader): Unit = { - import eu.medsea.mimeutil.{MimeUtil2, MimeType} - - val mimeUtil = new MimeUtil2() - mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") - - 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){ - dir.listFiles.foreach { file => - if(file.getName.indexOf('.') < 0){ - val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString - if(mimeType.startsWith("image/")){ - file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) - } - } - } - } - } - } - } - }, - Version(1, 13), - Version(1, 12), - Version(1, 11), - Version(1, 10), - Version(1, 9), - Version(1, 8), - Version(1, 7), - Version(1, 6), - Version(1, 5), - Version(1, 4), - new Version(1, 3){ - 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 => - defining(git.getRepository.getConfig){ config => - if(!config.getBoolean("http", "receivepack", false)){ - config.setBoolean("http", null, "receivepack", true) - config.save - } - } - } - } - } - }, - Version(1, 2), - Version(1, 1), - Version(1, 0), - Version(0, 0) - ) - - /** - * The head version of BitBucket. - */ - val headVersion = versions.head - - /** - * The version file (GITBUCKET_HOME/version). - */ - lazy val versionFile = new File(GitBucketHome, "version") - - /** - * Returns the current version from the version file. - */ - def getCurrentVersion(): Version = { - if(versionFile.exists){ - FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { - case Array(majorVersion, minorVersion) => { - versions.find { v => - v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt - }.getOrElse(Version(0, 0)) - } - case _ => Version(0, 0) - } - } else Version(0, 0) - } - -} - -/** - * Update database schema automatically in the context initializing. - */ -class AutoUpdateListener extends ServletContextListener { - import AutoUpdate._ - - private val logger = LoggerFactory.getLogger(classOf[AutoUpdateListener]) - - override def contextInitialized(event: ServletContextEvent): Unit = { - val dataDir = event.getServletContext.getInitParameter("gitbucket.home") - if(dataDir != null){ - System.setProperty("gitbucket.home", dataDir) - } - org.h2.Driver.load() - - defining(getConnection()){ conn => - // Migration - logger.debug("Start schema update") - Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn => - FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") - } - // Load plugins - logger.debug("Initialize plugins") - PluginRegistry.initialize(conn) - } - - } - - def contextDestroyed(sce: ServletContextEvent): Unit = { - // Shutdown plugins - PluginRegistry.shutdown() - } - - private def getConnection(): Connection = - DriverManager.getConnection( - DatabaseConfig.url, - DatabaseConfig.user, - DatabaseConfig.password) - -} diff --git a/src/main/scala/servlet/InitializeListener.scala b/src/main/scala/servlet/InitializeListener.scala new file mode 100644 index 0000000..fdc3c48 --- /dev/null +++ b/src/main/scala/servlet/InitializeListener.scala @@ -0,0 +1,204 @@ +package servlet + +import java.io.File +import java.sql.{DriverManager, Connection} +import org.apache.commons.io.FileUtils +import javax.servlet.{ServletContextListener, ServletContextEvent} +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 { + + /** + * The history of versions. A head of this sequence is the current BitBucket version. + */ + val versions = Seq( + new Version(2, 8), + new Version(2, 7) { + 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") + val repoName = rs.getString("REPOSITORY_NAME") + defining(Directory.getAttachedDir(userName, repoName)){ newDir => + val oldDir = new File(newDir.getParentFile, "issues") + if(oldDir.exists && oldDir.isDirectory){ + oldDir.renameTo(newDir) + } + } + // Update ORIGIN_USER_NAME and ORIGIN_REPOSITORY_NAME if it does not exist + val originalUserName = rs.getString("ORIGIN_USER_NAME") + val originalRepoName = rs.getString("ORIGIN_REPOSITORY_NAME") + if(originalUserName != null && originalRepoName != null){ + if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", + originalUserName, originalRepoName) == 0){ + conn.update("UPDATE REPOSITORY SET ORIGIN_USER_NAME = NULL, ORIGIN_REPOSITORY_NAME = NULL " + + "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) + } + } + // Update PARENT_USER_NAME and PARENT_REPOSITORY_NAME if it does not exist + val parentUserName = rs.getString("PARENT_USER_NAME") + val parentRepoName = rs.getString("PARENT_REPOSITORY_NAME") + if(parentUserName != null && parentRepoName != null){ + if(conn.selectInt("SELECT COUNT(*) FROM REPOSITORY WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", + parentUserName, parentRepoName) == 0){ + conn.update("UPDATE REPOSITORY SET PARENT_USER_NAME = NULL, PARENT_REPOSITORY_NAME = NULL " + + "WHERE USER_NAME = ? AND REPOSITORY_NAME = ?", userName, repoName) + } + } + } + } + }, + new Version(2, 6), + new Version(2, 5), + new Version(2, 4), + new Version(2, 3) { + 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") + if (curInfo != newInfo) { + conn.update("UPDATE ACTIVITY SET ADDITIONAL_INFO = ? WHERE ACTIVITY_ID = ?", newInfo, rs.getInt("ACTIVITY_ID")) + } + } + 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, cl: ClassLoader): Unit = { + import eu.medsea.mimeutil.{MimeUtil2, MimeType} + + val mimeUtil = new MimeUtil2() + mimeUtil.registerMimeDetector("eu.medsea.mimeutil.detector.MagicMimeMimeDetector") + + 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){ + dir.listFiles.foreach { file => + if(file.getName.indexOf('.') < 0){ + val mimeType = MimeUtil2.getMostSpecificMimeType(mimeUtil.getMimeTypes(file, new MimeType("application/octet-stream"))).toString + if(mimeType.startsWith("image/")){ + file.renameTo(new File(file.getParent, file.getName + "." + mimeType.split("/")(1))) + } + } + } + } + } + } + } + }, + Version(1, 13), + Version(1, 12), + Version(1, 11), + Version(1, 10), + Version(1, 9), + Version(1, 8), + Version(1, 7), + Version(1, 6), + Version(1, 5), + Version(1, 4), + new Version(1, 3){ + 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 => + defining(git.getRepository.getConfig){ config => + if(!config.getBoolean("http", "receivepack", false)){ + config.setBoolean("http", null, "receivepack", true) + config.save + } + } + } + } + } + }, + Version(1, 2), + Version(1, 1), + Version(1, 0), + Version(0, 0) + ) + + /** + * The head version of BitBucket. + */ + val headVersion = versions.head + + /** + * The version file (GITBUCKET_HOME/version). + */ + lazy val versionFile = new File(GitBucketHome, "version") + + /** + * Returns the current version from the version file. + */ + def getCurrentVersion(): Version = { + if(versionFile.exists){ + FileUtils.readFileToString(versionFile, "UTF-8").trim.split("\\.") match { + case Array(majorVersion, minorVersion) => { + versions.find { v => + v.majorVersion == majorVersion.toInt && v.minorVersion == minorVersion.toInt + }.getOrElse(Version(0, 0)) + } + case _ => Version(0, 0) + } + } else Version(0, 0) + } + +} + +/** + * Initialize GitBucket system. + * Update database schema and load plug-ins automatically in the context initializing. + */ +class InitializeListener extends ServletContextListener { + import AutoUpdate._ + + private val logger = LoggerFactory.getLogger(classOf[InitializeListener]) + + override def contextInitialized(event: ServletContextEvent): Unit = { + val dataDir = event.getServletContext.getInitParameter("gitbucket.home") + if(dataDir != null){ + System.setProperty("gitbucket.home", dataDir) + } + org.h2.Driver.load() + + defining(getConnection()){ conn => + // Migration + logger.debug("Start schema update") + Versions.update(conn, headVersion, getCurrentVersion(), versions, Thread.currentThread.getContextClassLoader){ conn => + FileUtils.writeStringToFile(versionFile, headVersion.versionString, "UTF-8") + } + // Load plugins + logger.debug("Initialize plugins") + PluginRegistry.initialize(event.getServletContext, conn) + } + + } + + def contextDestroyed(event: ServletContextEvent): Unit = { + // Shutdown plugins + PluginRegistry.shutdown(event.getServletContext) + } + + private def getConnection(): Connection = + DriverManager.getConnection( + DatabaseConfig.url, + DatabaseConfig.user, + DatabaseConfig.password) + +} diff --git a/src/main/scala/servlet/PluginActionFilter.scala b/src/main/scala/servlet/PluginActionFilter.scala deleted file mode 100644 index 1ef7642..0000000 --- a/src/main/scala/servlet/PluginActionFilter.scala +++ /dev/null @@ -1,60 +0,0 @@ -package servlet - -import javax.servlet._ -import javax.servlet.http.{HttpServletResponse, HttpServletRequest} - -import model.Account -import play.twirl.api.Html -import plugin.PluginRegistry -import service.SystemSettingsService -import util.Keys -import app.Context -import plugin.Results._ -import plugin.Sessions._ - -class PluginActionFilter extends Filter with SystemSettingsService { - - def init(config: FilterConfig) = {} - - def destroy(): Unit = {} - - def doFilter(req: ServletRequest, res: ServletResponse, chain: FilterChain): Unit = (req, res) match { - case (req: HttpServletRequest, res: HttpServletResponse) => { - val method = req.getMethod.toLowerCase - val path = req.getRequestURI.substring(req.getContextPath.length) - val registry = PluginRegistry() - - registry.getGlobalAction(method, path).map { action => - // Create Context - val loginAccount = req.getSession.getAttribute(Keys.Session.LoginAccount).asInstanceOf[Account] - implicit val context = Context(loadSystemSettings(), Option(loginAccount), req) - sessions.set(Database.getSession(req)) - try { - // Invoke global action - action(req, res, context) match { - // TODO to be type classes? - case x: String => - res.setContentType("text/plain; charset=UTF-8") - res.getWriter.write(x) - res.getWriter.flush() - case x: Html => - res.setContentType("text/html; charset=UTF-8") - // TODO title of plugin action - res.getWriter.write(html.main("TODO")(x).body) - res.getWriter.flush() - case Redirect(x) => - res.sendRedirect(x) - case Fragment(x) => - res.getWriter.write(x.body) - res.getWriter.flush() - } - } finally { - sessions.remove() - } - }.getOrElse { - chain.doFilter(req, res) - } - } - } - -} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml index 51513a1..c015d24 100644 --- a/src/main/webapp/WEB-INF/web.xml +++ b/src/main/webapp/WEB-INF/web.xml @@ -12,10 +12,10 @@ - + - servlet.AutoUpdateListener + servlet.InitializeListener