You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@toree.apache.org by lb...@apache.org on 2016/02/29 18:44:16 UTC

[1/3] incubator-toree git commit: Added initial plugin implementation

Repository: incubator-toree
Updated Branches:
  refs/heads/master 085c2974c -> 4c0dccfb7


http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/TestPlugin.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/TestPlugin.scala b/plugins/src/test/scala/test/utils/TestPlugin.scala
new file mode 100644
index 0000000..a3b43bf
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/TestPlugin.scala
@@ -0,0 +1,52 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+import org.apache.toree.plugins.Plugin
+import org.apache.toree.plugins.annotations.{Destroy, Events, Event, Init}
+
+/**
+ * Test plugin that provides an implementation to a plugin.
+ *
+ * @note Exists in global space instead of nested in test classes due to the
+ *       fact that Scala creates a non-nullary constructor when a class is
+ *       nested.
+ */
+class TestPlugin extends Plugin {
+  type Callback = () => Any
+  private var initCallbacks = collection.mutable.Seq[Callback]()
+  private var eventCallbacks = collection.mutable.Seq[Callback]()
+  private var eventsCallbacks = collection.mutable.Seq[Callback]()
+  private var destroyCallbacks = collection.mutable.Seq[Callback]()
+
+  def addInitCallback(callback: Callback) = initCallbacks :+= callback
+  def addEventCallback(callback: Callback) = eventCallbacks :+= callback
+  def addEventsCallback(callback: Callback) = eventsCallbacks :+= callback
+  def addDestroyCallback(callback: Callback) = destroyCallbacks :+= callback
+
+  @Init def initMethod() = initCallbacks.map(_())
+  @Event(name = "event1") def eventMethod() = eventCallbacks.map(_())
+  @Events(names = Array("event2", "event3")) def eventsMethod() =
+    eventsCallbacks.map(_())
+  @Destroy def destroyMethod() = destroyCallbacks.map(_())
+}
+
+object TestPlugin {
+  val DefaultEvent = "event1"
+  val DefaultEvents1 = "event2"
+  val DefaultEvents2 = "event3"
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/TestPluginWithDependencies.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/TestPluginWithDependencies.scala b/plugins/src/test/scala/test/utils/TestPluginWithDependencies.scala
new file mode 100644
index 0000000..a413aad
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/TestPluginWithDependencies.scala
@@ -0,0 +1,59 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+import org.apache.toree.plugins.Plugin
+import org.apache.toree.plugins.annotations.{Destroy, Event, Events, Init}
+
+/**
+ * Test plugin that provides an implementation to a plugin.
+ *
+ * @note Exists in global space instead of nested in test classes due to the
+ *       fact that Scala creates a non-nullary constructor when a class is
+ *       nested.
+ */
+class TestPluginWithDependencies extends Plugin {
+  type Callback = (TestPluginDependency) => Any
+  private var initCallbacks = collection.mutable.Seq[Callback]()
+  private var eventCallbacks = collection.mutable.Seq[Callback]()
+  private var eventsCallbacks = collection.mutable.Seq[Callback]()
+  private var destroyCallbacks = collection.mutable.Seq[Callback]()
+
+  def addInitCallback(callback: Callback) = initCallbacks :+= callback
+  def addEventCallback(callback: Callback) = eventCallbacks :+= callback
+  def addEventsCallback(callback: Callback) = eventsCallbacks :+= callback
+  def addDestroyCallback(callback: Callback) = destroyCallbacks :+= callback
+
+  @Init def initMethod(d: TestPluginDependency) = initCallbacks.map(_(d))
+
+  @Event(name = "event1")
+  def eventMethod(d: TestPluginDependency) = eventCallbacks.map(_(d))
+
+  @Events(names = Array("event2", "event3"))
+  def eventsMethod(d: TestPluginDependency) = eventsCallbacks.map(_(d))
+
+  @Destroy def destroyMethod(d: TestPluginDependency) =
+    destroyCallbacks.map(_(d))
+}
+
+case class TestPluginDependency(value: Int)
+
+object TestPluginWithDependencies {
+  val DefaultEvent = "event1"
+  val DefaultEvents1 = "event2"
+  val DefaultEvents2 = "event3"
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/project/Build.scala
----------------------------------------------------------------------
diff --git a/project/Build.scala b/project/Build.scala
index 46e732b..baf8baf 100644
--- a/project/Build.scala
+++ b/project/Build.scala
@@ -56,7 +56,7 @@ object Build extends Build with Settings with SubProjects with TestTasks {
   ).aggregate(
     client, kernel, kernel_api, communication, protocol, macros,
     pyspark_interpreter, scala_interpreter, sparkr_interpreter,
-    sql_interpreter
+    sql_interpreter, plugins
   ).dependsOn(
     client % "test->test",
     kernel % "test->test"
@@ -119,6 +119,7 @@ trait SubProjects extends Settings with TestTasks {
     base = file("pyspark-interpreter"),
     settings = fullSettings
   )) dependsOn(
+    plugins % "test->test;compile->compile",
     protocol % "test->test;compile->compile",
     kernel_api % "test->test;compile->compile"
   )
@@ -131,6 +132,7 @@ trait SubProjects extends Settings with TestTasks {
     base = file("scala-interpreter"),
     settings = fullSettings
   )) dependsOn(
+    plugins % "test->test;compile->compile",
     protocol % "test->test;compile->compile",
     kernel_api % "test->test;compile->compile"
   )
@@ -143,6 +145,7 @@ trait SubProjects extends Settings with TestTasks {
     base = file("sparkr-interpreter"),
     settings = fullSettings
   )) dependsOn(
+    plugins % "test->test;compile->compile",
     protocol % "test->test;compile->compile",
     kernel_api % "test->test;compile->compile"
   )
@@ -155,6 +158,7 @@ trait SubProjects extends Settings with TestTasks {
     base = file("sql-interpreter"),
     settings = fullSettings
   )) dependsOn(
+    plugins % "test->test;compile->compile",
     protocol % "test->test;compile->compile",
     kernel_api % "test->test;compile->compile"
   )
@@ -167,7 +171,10 @@ trait SubProjects extends Settings with TestTasks {
     id = "toree-kernel-api",
     base = file("kernel-api"),
     settings = fullSettings
-  )) dependsOn(macros % "test->test;compile->compile")
+  )) dependsOn(
+    plugins % "test->test;compile->compile",
+    macros % "test->test;compile->compile"
+  )
 
   /**
    * Required by the sbt-buildinfo plugin. Defines the following:
@@ -214,6 +221,17 @@ trait SubProjects extends Settings with TestTasks {
   )) dependsOn(macros % "test->test;compile->compile")
 
   /**
+   * Project representing base plugin system for the Toree infrastructure.
+   */
+  lazy val plugins = addTestTasksToProject(Project(
+    id = "toree-plugins",
+    base = file("plugins"),
+    settings = fullSettings
+  )) dependsOn(
+    macros % "test->test;compile->compile"
+  )
+
+  /**
    * Project representing macros in Scala that must be compiled separately from
    * any other project using them.
    */

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/project/Common.scala
----------------------------------------------------------------------
diff --git a/project/Common.scala b/project/Common.scala
index c70fbe8..5ea12aa 100644
--- a/project/Common.scala
+++ b/project/Common.scala
@@ -102,7 +102,8 @@ object Common {
     ),
 
     // Java-based options for compilation (all tasks)
-    javacOptions in Compile ++= Seq(""),
+    // NOTE: Providing a blank flag causes failures, only uncomment with options
+    //javacOptions in Compile ++= Seq(""),
 
     // Java-based options for just the compile task
     javacOptions in (Compile, compile) ++= Seq(


[2/3] incubator-toree git commit: Added initial plugin implementation

Posted by lb...@apache.org.
http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/PluginManagerSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/PluginManagerSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/PluginManagerSpec.scala
new file mode 100644
index 0000000..09e0f10
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/PluginManagerSpec.scala
@@ -0,0 +1,532 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.io.File
+
+import org.apache.toree.plugins.dependencies.DependencyManager
+import org.mockito.Matchers._
+import org.mockito.Mockito._
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{FunSpec, Matchers, OneInstancePerTest}
+import test.utils._
+
+import scala.util.{Failure, Success}
+
+class PluginManagerSpec extends FunSpec with Matchers
+  with OneInstancePerTest with MockitoSugar
+{
+  private val TestPluginName = "some.plugin.class.name"
+
+  private val mockPluginClassLoader = mock[PluginClassLoader]
+  private val mockPluginSearcher = mock[PluginSearcher]
+  private val mockDependencyManager = mock[DependencyManager]
+  private val pluginManager = new PluginManager(
+    pluginClassLoader = mockPluginClassLoader,
+    pluginSearcher    = mockPluginSearcher,
+    dependencyManager = mockDependencyManager
+  )
+
+  describe("PluginManager") {
+    describe("#isActive") {
+      it("should return true if a plugin is loaded") {
+        val classInfoList = Seq(
+          TestClassInfo(
+            name = classOf[TestPlugin].getName,
+            location = new File("some/path/to/file.jar")
+          )
+        )
+
+        // When returning class information
+        doReturn(classInfoList.toIterator)
+          .when(mockPluginSearcher).search(any[File])
+
+        doReturn(classOf[TestPlugin])
+          .when(mockPluginClassLoader).loadClass(anyString())
+
+        // Perform the loading of plugins
+        pluginManager.loadPlugins(mock[File])
+
+        // Verify expected plugin has been loaded and is active
+        pluginManager.isActive(classOf[TestPlugin].getName) should be (true)
+      }
+
+      it("should return false if a plugin is not loaded") {
+        pluginManager.isActive(TestPluginName)
+      }
+    }
+
+    describe("#plugins") {
+      it("should return an iterator over all active plugins") {
+        val classInfoList = Seq(
+          TestClassInfo(
+            name = classOf[TestPlugin].getName,
+            location = new File("some/path/to/file.jar")
+          )
+        )
+
+        // When returning class information
+        doReturn(classInfoList.toIterator)
+          .when(mockPluginSearcher).search(any[File])
+
+        doReturn(classOf[TestPlugin])
+          .when(mockPluginClassLoader).loadClass(anyString())
+
+        // Perform the loading of plugins
+        pluginManager.loadPlugins(mock[File])
+
+        // Verify that we have plugins loaded
+        pluginManager.plugins should have size 1
+      }
+
+      it("should be empty if no plugins are loaded") {
+        pluginManager.plugins should be (empty)
+      }
+    }
+
+    describe("#loadPlugin") {
+      it("should return the same plugin if one exists with matching name") {
+        val c = classOf[TestPlugin]
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+
+        // If name matches, doesn't try to create class
+        pluginManager.loadPlugin(c.getName, null).get should be (plugin)
+      }
+
+      it("should create a new instance of the class and return it as a plugin") {
+        val p = pluginManager.loadPlugin("name", classOf[TestPlugin])
+
+        p.get shouldBe a [TestPlugin]
+      }
+
+      it("should set the internal plugin manager of the new plugin") {
+        val p = pluginManager.loadPlugin("name", classOf[TestPlugin])
+
+        p.get.pluginManager should be (pluginManager)
+      }
+
+      it("should add the new plugin to the list of active plugins") {
+        val c = classOf[TestPlugin]
+        val name = c.getName
+        val plugin = pluginManager.loadPlugin(name, c).get
+
+        pluginManager.isActive(name) should be (true)
+        pluginManager.findPlugin(name).get should be (plugin)
+      }
+
+      it("should return a failure if unable to create the class instance") {
+        class SomeClass(x: Int) extends Plugin
+        val p = pluginManager.loadPlugin("", classOf[SomeClass])
+
+        p.isFailure should be (true)
+        p.failed.get shouldBe an [InstantiationException]
+      }
+
+      it("should return an unknown plugin type failure if created class but not a plugin") {
+        // NOTE: Must use global class (not nested) to find one with empty constructor
+        val p = pluginManager.loadPlugin("", classOf[NotAPlugin])
+
+        p.isFailure should be (true)
+        p.failed.get shouldBe an [UnknownPluginTypeException]
+      }
+    }
+
+    describe("#loadPlugins") {
+      it("should load nothing if the plugin searcher returns empty handed") {
+        val expected = Nil
+
+        doReturn(Iterator.empty).when(mockPluginSearcher).search(any[File])
+        val actual = pluginManager.loadPlugins(mock[File])
+
+        actual should be (expected)
+      }
+
+      it("should add paths containing plugins to the plugin class loader") {
+        val classInfoList = Seq(
+          TestClassInfo(
+            name = "some.class",
+            location = new File("some/path/to/file.jar")
+          )
+        )
+
+        // When returning class information
+        doReturn(classInfoList.toIterator)
+          .when(mockPluginSearcher).search(any[File])
+
+        doReturn(classOf[TestPlugin])
+          .when(mockPluginClassLoader).loadClass(anyString())
+
+        // Perform the loading of plugins
+        pluginManager.loadPlugins(mock[File])
+
+        // Should add the locations from class information
+        classInfoList.map(_.location.toURI.toURL)
+          .foreach(verify(mockPluginClassLoader).addURL)
+      }
+
+      it("should load the plugin classes as external plugins") {
+        val classInfoList = Seq(
+          TestClassInfo(
+            name = classOf[TestPlugin].getName,
+            location = new File("some/path/to/file.jar")
+          )
+        )
+
+        // When returning class information
+        doReturn(classInfoList.toIterator)
+          .when(mockPluginSearcher).search(any[File])
+
+        doReturn(classOf[TestPlugin])
+          .when(mockPluginClassLoader).loadClass(anyString())
+
+        // Perform the loading of plugins
+        val plugins = pluginManager.loadPlugins(mock[File])
+
+        // Should contain a new instance of our test plugin class
+        plugins should have length 1
+        plugins.head shouldBe a [TestPlugin]
+      }
+    }
+
+    describe("#initializePlugins") {
+      it("should send the initialize event to the specified plugins") {
+        val testPlugin = new TestPlugin
+
+        @volatile var called = false
+        testPlugin.addInitCallback(() => called = true)
+
+        pluginManager.initializePlugins(Seq(testPlugin))
+        called should be (true)
+      }
+
+      it("should include any scoped dependencies in the initialized event") {
+        val testPlugin = new TestPluginWithDependencies
+        val dependency = TestPluginDependency(999)
+
+        @volatile var called = false
+        @volatile var d: TestPluginDependency = null
+        testPlugin.addInitCallback((d2) => {
+          called = true
+          d = d2
+        })
+
+        val dependencyManager = new DependencyManager
+        dependencyManager.add(dependency)
+
+        doReturn(dependencyManager).when(mockDependencyManager)
+          .merge(dependencyManager)
+
+        pluginManager.initializePlugins(Seq(testPlugin), dependencyManager)
+        called should be (true)
+        d should be (dependency)
+      }
+
+      it("should return a collection of successes for each plugin method") {
+        val testPlugin = new TestPlugin
+
+        val results = pluginManager.initializePlugins(Seq(testPlugin))
+        results should have size 1
+        results.head.pluginName should be (testPlugin.name)
+        results.head.isSuccess should be (true)
+      }
+
+      it("should return failures for any failed plugin method") {
+        val testPlugin = new TestPlugin
+        testPlugin.addInitCallback(() => throw new Throwable)
+
+        val results = pluginManager.initializePlugins(Seq(testPlugin))
+        results should have size 1
+        results.head.pluginName should be (testPlugin.name)
+        results.head.isFailure should be (true)
+      }
+    }
+
+    describe("#findPlugin") {
+      it("should return Some(plugin) if a plugin with matching name is found") {
+        val c = classOf[TestPlugin]
+        val p = pluginManager.loadPlugin(c.getName, c).get
+        pluginManager.findPlugin(p.name) should be (Some(p))
+      }
+
+      it("should return None if no plugin with matching name is found") {
+        pluginManager.findPlugin("some.class") should be (None)
+      }
+    }
+
+    describe("#destroyPlugins") {
+      it("should send the destroy event to the specified plugins") {
+        val testPlugin = new TestPlugin
+
+        @volatile var called = false
+        testPlugin.addDestroyCallback(() => called = true)
+
+        pluginManager.destroyPlugins(Seq(testPlugin))
+        called should be (true)
+      }
+
+      it("should include any scoped dependencies in the destroy event") {
+        val testPlugin = new TestPluginWithDependencies
+        val dependency = TestPluginDependency(999)
+
+        @volatile var called = false
+        @volatile var d: TestPluginDependency = null
+        testPlugin.addDestroyCallback((d2) => {
+          called = true
+          d = d2
+        })
+
+        val dependencyManager = new DependencyManager
+        dependencyManager.add(dependency)
+
+        doReturn(dependencyManager).when(mockDependencyManager)
+          .merge(dependencyManager)
+
+        pluginManager.destroyPlugins(Seq(testPlugin), dependencyManager)
+        called should be (true)
+        d should be (dependency)
+      }
+
+      it("should return a collection of successes for each plugin method") {
+        val testPlugin = new TestPlugin
+
+        val results = pluginManager.destroyPlugins(Seq(testPlugin))
+        results should have size 1
+        results.head.pluginName should be (testPlugin.name)
+        results.head.isSuccess should be (true)
+      }
+
+      it("should return failures for any failed plugin method") {
+        val testPlugin = new TestPlugin
+        testPlugin.addDestroyCallback(() => throw new Throwable)
+
+        val results = pluginManager.destroyPlugins(Seq(testPlugin))
+        results should have size 1
+        results.head.pluginName should be (testPlugin.name)
+        results.head.isFailure should be (true)
+      }
+
+      it("should remove any plugin that is successfully destroyed") {
+        val c = classOf[TestPlugin]
+
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+        pluginManager.plugins should have size 1
+
+        pluginManager.destroyPlugins(Seq(plugin))
+        pluginManager.plugins should be (empty)
+      }
+
+      it("should remove any plugin that fails if destroyOnFailure is true") {
+        val c = classOf[TestPlugin]
+
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+        pluginManager.plugins should have size 1
+
+        val testPlugin = plugin.asInstanceOf[TestPlugin]
+        testPlugin.addDestroyCallback(() => throw new Throwable)
+
+        pluginManager.destroyPlugins(Seq(plugin))
+        pluginManager.plugins should be (empty)
+      }
+
+      it("should not remove the plugin from the list if a destroy callback fails and destroyOnFailure is false") {
+        val c = classOf[TestPlugin]
+
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+        pluginManager.plugins should have size 1
+
+        val testPlugin = plugin.asInstanceOf[TestPlugin]
+        testPlugin.addDestroyCallback(() => throw new Throwable)
+
+        pluginManager.destroyPlugins(Seq(plugin), destroyOnFailure = false)
+        pluginManager.plugins should contain (testPlugin)
+      }
+    }
+
+    describe("#firstEventFirstResult") {
+      it("should return Some(PluginMethodResult) with first result in list if non-empty") {
+        lazy val expected = Some(SuccessPluginMethodResult(
+          testPlugin.eventMethodMap(TestPlugin.DefaultEvent).head,
+          Seq("first")
+        ))
+
+        lazy val testPlugin = pluginManager.loadPlugin(
+          classOf[TestPlugin].getName, classOf[TestPlugin]
+        ).get.asInstanceOf[TestPlugin]
+        testPlugin.addEventCallback(() => "first")
+
+        lazy val testPluginWithDependencies = pluginManager.loadPlugin(
+          classOf[TestPluginWithDependencies].getName,
+          classOf[TestPluginWithDependencies]
+        ).get.asInstanceOf[TestPluginWithDependencies]
+        testPluginWithDependencies.addEventCallback(_ => "last")
+
+        val actual = pluginManager.fireEventFirstResult(TestPlugin.DefaultEvent)
+
+        actual should be (expected)
+      }
+
+      it("should return None if list is empty") {
+        val expected = None
+
+        val actual = pluginManager.fireEventFirstResult(TestPlugin.DefaultEvent)
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#firstEventLastResult") {
+      it("should return Some(Try(result)) with last result in list if non-empty") {
+        lazy val expected = Some(SuccessPluginMethodResult(
+          testPluginWithDependencies.eventMethodMap(TestPlugin.DefaultEvent).head,
+          Seq("last")
+        ))
+
+        lazy val testPlugin = pluginManager.loadPlugin(
+          classOf[TestPlugin].getName, classOf[TestPlugin]
+        ).get.asInstanceOf[TestPlugin]
+        testPlugin.addEventCallback(() => "first")
+
+        lazy val testPluginWithDependencies = pluginManager.loadPlugin(
+          classOf[TestPluginWithDependencies].getName,
+          classOf[TestPluginWithDependencies]
+        ).get.asInstanceOf[TestPluginWithDependencies]
+        testPluginWithDependencies.addEventCallback(_ => "last")
+
+        val dm = new DependencyManager
+        dm.add(TestPluginDependency(999))
+
+        doReturn(dm).when(mockDependencyManager).merge(any[DependencyManager])
+
+        val actual = pluginManager.fireEventLastResult(
+          TestPlugin.DefaultEvent, dm.toSeq: _*
+        )
+
+        actual should be (expected)
+      }
+
+      it("should return None if list is empty") {
+        val expected = None
+
+        val actual = pluginManager.fireEventLastResult(TestPlugin.DefaultEvent)
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#fireEvent") {
+      it("should invoke any plugin methods listening for the event") {
+        val c = classOf[TestPlugin]
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+        val testPlugin = plugin.asInstanceOf[TestPlugin]
+
+        @volatile var called = 0
+        @volatile var calledMulti = 0
+        testPlugin.addEventCallback(() => called += 1)
+        testPlugin.addEventsCallback(() => calledMulti += 1)
+
+        pluginManager.fireEvent(TestPlugin.DefaultEvent)
+        pluginManager.fireEvent(TestPlugin.DefaultEvents1)
+        pluginManager.fireEvent(TestPlugin.DefaultEvents2)
+
+        called should be (1)
+        calledMulti should be (2)
+      }
+
+      it("should include any scoped dependencies in the fired event") {
+        val c = classOf[TestPluginWithDependencies]
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+        val testPlugin = plugin.asInstanceOf[TestPluginWithDependencies]
+        val dependency = TestPluginDependency(999)
+        val dependencyManager = new DependencyManager
+        dependencyManager.add(dependency)
+
+        @volatile var called = 0
+        @volatile var calledMulti = 0
+        testPlugin.addEventCallback((d) => {
+          d should be (dependency)
+          called += 1
+        })
+        testPlugin.addEventsCallback((d) => {
+          d should be (dependency)
+          calledMulti += 1
+        })
+
+        doReturn(dependencyManager).when(mockDependencyManager)
+          .merge(dependencyManager)
+        pluginManager.fireEvent(TestPlugin.DefaultEvent, dependencyManager)
+
+        doReturn(dependencyManager).when(mockDependencyManager)
+          .merge(dependencyManager)
+        pluginManager.fireEvent(TestPlugin.DefaultEvents1, dependencyManager)
+
+        doReturn(dependencyManager).when(mockDependencyManager)
+          .merge(dependencyManager)
+        pluginManager.fireEvent(TestPlugin.DefaultEvents2, dependencyManager)
+
+        called should be (1)
+        calledMulti should be (2)
+      }
+
+      it("should return a collection of results for invoked plugin methods") {
+        val c = classOf[TestPlugin]
+        val plugin = pluginManager.loadPlugin(c.getName, c).get
+        val testPlugin = plugin.asInstanceOf[TestPlugin]
+
+        testPlugin.addEventCallback(() => {})
+        testPlugin.addEventsCallback(() => throw new Throwable)
+
+        val r1 = pluginManager.fireEvent(TestPlugin.DefaultEvent)
+        val r2 = pluginManager.fireEvent(TestPlugin.DefaultEvents1)
+
+        r1 should have size 1
+        r1.head.isSuccess should be (true)
+
+        r2 should have size 1
+        r2.head.isFailure should be (true)
+      }
+
+      it("should return results based on method and plugin priority") {
+        val testPlugin = {
+          val c = classOf[TestPlugin]
+          val plugin = pluginManager.loadPlugin(c.getName, c).get
+          plugin.asInstanceOf[TestPlugin]
+        }
+
+        val priorityPlugin = {
+          val c = classOf[PriorityPlugin]
+          val plugin = pluginManager.loadPlugin(c.getName, c).get
+          plugin.asInstanceOf[PriorityPlugin]
+        }
+
+        // Order should be
+        // 1. eventMethod (priority)
+        // 2. eventMethod (test)
+        // 3. eventMethod2 (priority)
+        val r = pluginManager.fireEvent(TestPlugin.DefaultEvent)
+
+        r.head.pluginName should be (priorityPlugin.name)
+        r.head.methodName should be ("eventMethod")
+
+        r(1).pluginName should be (testPlugin.name)
+        r(1).methodName should be ("eventMethod")
+
+        r(2).pluginName should be (priorityPlugin.name)
+        r(2).methodName should be ("eventMethod2")
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodResultSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodResultSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodResultSpec.scala
new file mode 100644
index 0000000..0589aa7
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodResultSpec.scala
@@ -0,0 +1,151 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.lang.reflect.Method
+
+import org.apache.toree.plugins.annotations.{Priority, Event}
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+import org.mockito.Mockito._
+
+import scala.util.{Failure, Success, Try}
+
+class PluginMethodResultSpec extends FunSpec with Matchers
+  with OneInstancePerTest with MockitoSugar
+{
+  private val testResult = new AnyRef
+  private val testThrowable = new Throwable
+
+  @Priority(level = 998)
+  private class TestPlugin extends Plugin {
+    @Priority(level = 999)
+    @Event(name = "success")
+    def success() = testResult
+
+    @Event(name = "failure")
+    def failure() = throw testThrowable
+  }
+
+  private val testPlugin = new TestPlugin
+
+  private val successResult: PluginMethodResult = SuccessPluginMethodResult(
+    testPlugin.eventMethodMap("success").head,
+    testResult
+  )
+
+  private val failureResult: PluginMethodResult = FailurePluginMethodResult(
+    testPlugin.eventMethodMap("failure").head,
+    testThrowable
+  )
+
+  describe("PluginMethodResult") {
+    describe("#pluginName") {
+      it("should return the name of the plugin from the invoked plugin method") {
+        val expected = testPlugin.name
+
+        val actual = successResult.pluginName
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#methodName") {
+      it("should return the name of the method from the invoked plugin method") {
+        val expected = "success"
+
+        val actual = successResult.methodName
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#pluginPriority") {
+      it("should return the priority of the plugin from the invoked plugin method") {
+        val expected = 998
+
+        val actual = successResult.pluginPriority
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#methodPriority") {
+      it("should return the priority of the method from the invoked plugin method") {
+        val expected = 999
+
+        val actual = successResult.methodPriority
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#isSuccess") {
+      it("should return true if representing a success result") {
+        val expected = true
+
+        val actual = successResult.isSuccess
+
+        actual should be (expected)
+      }
+
+      it("should return false if representing a failure result") {
+        val expected = false
+
+        val actual = failureResult.isSuccess
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#isFailure") {
+      it("should return false if representing a success result") {
+        val expected = false
+
+        val actual = successResult.isFailure
+
+        actual should be (expected)
+      }
+
+      it("should return true if representing a failure result") {
+        val expected = true
+
+        val actual = failureResult.isFailure
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#toTry") {
+      it("should return Success(result) if representing a success result") {
+        val expected = Success(testResult)
+
+        val actual = successResult.toTry
+
+        actual should be (expected)
+      }
+
+      it("should return Failure(throwable) if representing a failure result") {
+        val expected = Failure(testThrowable)
+
+        val actual = failureResult.toTry
+
+        actual should be (expected)
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodSpec.scala
new file mode 100644
index 0000000..ae7c334
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/PluginMethodSpec.scala
@@ -0,0 +1,319 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package org.apache.toree.plugins
+
+import org.apache.toree.plugins.annotations._
+import org.apache.toree.plugins.dependencies.{DepClassNotFoundException, DepUnexpectedClassException, DepNameNotFoundException}
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+
+class PluginMethodSpec extends FunSpec with Matchers with OneInstancePerTest {
+  private val testThrowable = new Throwable
+  private case class TestDependency(x: Int)
+  private class TestPlugin extends Plugin {
+    @Init def initMethod() = {}
+    @Event(name = "event1") def eventMethod() = {}
+    @Events(names = Array("event2", "event3")) def eventsMethod() = {}
+    @Destroy def destroyMethod() = {}
+
+    @Event(name = "event1") @Events(names = Array("event2", "event3"))
+    def allEventsMethod() = {}
+
+    @Priority(level = 999) def priorityMethod() = {}
+
+    def dependencyMethod(testDependency: TestDependency) = testDependency
+
+    def namedDependencyMethod(
+      @DepName(name = "name") testDependency: TestDependency
+    ) = testDependency
+
+    def normalMethod() = {}
+
+    def badMethod() = throw testThrowable
+  }
+
+  private val testPlugin = new TestPlugin
+
+  describe("PluginMethod") {
+    describe("#eventNames") {
+      it("should return the event names associated with the method") {
+        val expected = Seq("event1", "event2", "event3")
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("allEventsMethod")
+        )
+
+        val actual = pluginMethod.eventNames
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#isInit") {
+      it("should return true if method is annotated with Init") {
+        val expected = true
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("initMethod")
+        )
+
+        val actual = pluginMethod.isInit
+
+        actual should be (expected)
+      }
+
+      it("should return false if method is not annotated with Init") {
+        val expected = false
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("normalMethod")
+        )
+
+        val actual = pluginMethod.isInit
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#isEvent") {
+      it("should return true if method is annotated with Event") {
+        val expected = true
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("eventMethod")
+        )
+
+        val actual = pluginMethod.isEvent
+
+        actual should be (expected)
+      }
+
+      it("should return false if method is not annotated with Event") {
+        val expected = false
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("normalMethod")
+        )
+
+        val actual = pluginMethod.isEvent
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#isEvents") {
+      it("should return true if method is annotated with Events") {
+        val expected = true
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("eventsMethod")
+        )
+
+        val actual = pluginMethod.isEvents
+
+        actual should be (expected)
+      }
+
+      it("should return false if method is not annotated with Events") {
+        val expected = false
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("normalMethod")
+        )
+
+        val actual = pluginMethod.isEvents
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#isDestroy") {
+      it("should return true if method is annotated with Destroy") {
+        val expected = true
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("destroyMethod")
+        )
+
+        val actual = pluginMethod.isDestroy
+
+        actual should be (expected)
+      }
+
+      it("should return false if method is not annotated with Destroy") {
+        val expected = false
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("normalMethod")
+        )
+
+        val actual = pluginMethod.isDestroy
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#priority") {
+      it("should return the priority level of the method if provided") {
+        val expected = 999
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("priorityMethod")
+        )
+
+        val actual = pluginMethod.priority
+
+        actual should be (expected)
+      }
+
+      it("should return the default priority level of the method if not provided") {
+        val expected = PluginMethod.DefaultPriority
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("normalMethod")
+        )
+
+        val actual = pluginMethod.priority
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#invoke") {
+      it("should return a failure of DepNameNotFound if named dependency missing") {
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod(
+            "namedDependencyMethod",
+            classOf[TestDependency]
+          )
+        )
+
+        import org.apache.toree.plugins.Implicits._
+        val result = pluginMethod.invoke(TestDependency(999))
+
+        result.toTry.failed.get shouldBe a [DepNameNotFoundException]
+      }
+
+      it("should return a failure of DepUnexpectedClass if named dependency found with wrong class") {
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod(
+            "namedDependencyMethod",
+            classOf[TestDependency]
+          )
+        )
+
+        import org.apache.toree.plugins.Implicits._
+        val result = pluginMethod.invoke("name" -> new AnyRef)
+
+        result.toTry.failed.get shouldBe a [DepUnexpectedClassException]
+      }
+
+      it("should return a failure of DepClassNotFound if no dependency with class found") {
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod(
+            "dependencyMethod",
+            classOf[TestDependency]
+          )
+        )
+
+        val result = pluginMethod.invoke()
+
+        result.toTry.failed.get shouldBe a [DepClassNotFoundException]
+      }
+
+      it("should return a failure of the underlying exception if an error encountered on invocation") {
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("badMethod")
+        )
+
+        val result = pluginMethod.invoke()
+
+        result.toTry.failed.get should be (testThrowable)
+      }
+
+      it("should return a success if invoked correctly") {
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod("normalMethod")
+        )
+
+        val result = pluginMethod.invoke()
+
+        result.isSuccess should be (true)
+      }
+
+      it("should be able to inject named dependencies") {
+        val expected = TestDependency(999)
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod(
+            "namedDependencyMethod",
+            classOf[TestDependency]
+          )
+        )
+
+        import org.apache.toree.plugins.Implicits._
+        val result = pluginMethod.invoke(
+          "name2" -> TestDependency(998),
+          "name" -> expected,
+          "name3" -> TestDependency(1000)
+        )
+        val actual = result.toTry.get
+
+        actual should be (expected)
+      }
+
+      it("should be able to inject dependencies") {
+        val expected = TestDependency(999)
+
+        val pluginMethod = PluginMethod(
+          testPlugin,
+          classOf[TestPlugin].getDeclaredMethod(
+            "dependencyMethod",
+            classOf[TestDependency]
+          )
+        )
+
+        import org.apache.toree.plugins.Implicits._
+        val result = pluginMethod.invoke(
+          "test",
+          expected,
+          Int.box(3)
+        )
+        val actual = result.toTry.get
+
+        actual should be (expected)
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/PluginSearcherSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/PluginSearcherSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/PluginSearcherSpec.scala
new file mode 100644
index 0000000..58231fb
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/PluginSearcherSpec.scala
@@ -0,0 +1,184 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.io.File
+
+import org.clapper.classutil.{Modifier, ClassFinder}
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+
+import org.mockito.Mockito._
+import test.utils.TestClassInfo
+
+class PluginSearcherSpec extends FunSpec with Matchers
+  with OneInstancePerTest with MockitoSugar
+{
+  private val mockClassFinder = mock[ClassFinder]
+  private val pluginSearcher = new PluginSearcher {
+    override protected def newClassFinder(): ClassFinder = mockClassFinder
+    override protected def newClassFinder(paths: Seq[File]): ClassFinder =
+      mockClassFinder
+  }
+
+  private val pluginClassInfo = TestClassInfo(
+    name = classOf[Plugin].getName,
+    modifiers = Set(Modifier.Interface)
+  )
+  private val directPluginClassInfo = TestClassInfo(
+    name = "direct.plugin",
+    superClassName = pluginClassInfo.name
+  )
+  private val directAsInterfacePluginClassInfo = TestClassInfo(
+    name = "direct.interface.plugin",
+    interfaces = List(pluginClassInfo.name)
+  )
+  private val indirectPluginClassInfo = TestClassInfo(
+    name = "indirect.plugin",
+    superClassName = directPluginClassInfo.name
+  )
+  private val indirectAsInterfacePluginClassInfo = TestClassInfo(
+    name = "indirect.interface.plugin",
+    interfaces = List(directAsInterfacePluginClassInfo.name)
+  )
+  private val traitPluginClassInfo = TestClassInfo(
+    name = "trait.plugin",
+    modifiers = Set(Modifier.Interface)
+  )
+  private val abstractClassPluginClassInfo = TestClassInfo(
+    name = "abstract.plugin",
+    modifiers = Set(Modifier.Abstract)
+  )
+  private val classInfos = Seq(
+    pluginClassInfo,
+    directPluginClassInfo, directAsInterfacePluginClassInfo,
+    indirectPluginClassInfo, indirectAsInterfacePluginClassInfo,
+    traitPluginClassInfo, abstractClassPluginClassInfo
+  )
+
+  describe("PluginSearcher") {
+    describe("#internal") {
+      it("should find any plugins directly extending the Plugin class") {
+        val expected = directPluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.internal.map(_.name)
+
+        actual should contain (expected)
+      }
+
+      it("should find any plugins directly extending the Plugin trait") {
+        val expected = directAsInterfacePluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.internal.map(_.name)
+
+        actual should contain (expected)
+      }
+
+      it("should find any plugins indirectly extending the Plugin class") {
+        val expected = indirectPluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.internal.map(_.name)
+
+        actual should contain (expected)
+      }
+
+      it("should find any plugins indirectly extending the Plugin trait") {
+        val expected = indirectAsInterfacePluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.internal.map(_.name)
+
+        actual should contain (expected)
+      }
+
+      it("should not include any traits or abstract classes") {
+        val expected = Seq(
+          abstractClassPluginClassInfo.name,
+          traitPluginClassInfo.name
+        )
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.internal.map(_.name)
+
+        actual should not contain atLeastOneOf (expected.head, expected.tail)
+      }
+    }
+
+    describe("#search") {
+      it("should find any plugins directly extending the Plugin class") {
+        val expected = directPluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.search().map(_.name).toSeq
+
+        actual should contain (expected)
+      }
+
+      it("should find any plugins directly extending the Plugin trait") {
+        val expected = directAsInterfacePluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.search().map(_.name).toSeq
+
+        actual should contain (expected)
+      }
+
+      it("should find any plugins indirectly extending the Plugin class") {
+        val expected = indirectPluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.search().map(_.name).toSeq
+
+        actual should contain (expected)
+      }
+
+      it("should find any plugins indirectly extending the Plugin trait") {
+        val expected = indirectAsInterfacePluginClassInfo.name
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.search().map(_.name).toSeq
+
+        actual should contain (expected)
+      }
+
+      it("should not include any traits or abstract classes") {
+        val expected = Seq(
+          abstractClassPluginClassInfo.name,
+          traitPluginClassInfo.name
+        )
+
+        doReturn(classInfos.toIterator).when(mockClassFinder).getClasses()
+
+        val actual = pluginSearcher.search().map(_.name).toSeq
+
+        actual should not contain atLeastOneOf (expected.head, expected.tail)
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/PluginSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/PluginSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/PluginSpec.scala
new file mode 100644
index 0000000..fd5e1f0
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/PluginSpec.scala
@@ -0,0 +1,327 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.lang.reflect.Method
+
+import org.apache.toree.plugins.annotations._
+import org.apache.toree.plugins.dependencies.DependencyManager
+import org.scalatest.mock.MockitoSugar
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+import org.mockito.Mockito._
+import org.mockito.Matchers.{eq => mockEq, _}
+import scala.reflect.runtime.universe._
+
+class PluginSpec extends FunSpec with Matchers with OneInstancePerTest with MockitoSugar {
+  private val mockPluginManager = mock[PluginManager]
+  private val testPlugin = {
+    val plugin = new TestPlugin
+    plugin.pluginManager_=(mockPluginManager)
+    plugin
+  }
+  private val extendedTestPlugin = {
+    val extendedPlugin = new ExtendedTestPlugin
+    extendedPlugin.pluginManager_=(mockPluginManager)
+    extendedPlugin
+  }
+  private val registerPlugin = new RegisterPlugin
+
+  @Priority(level = 999) private class PriorityPlugin extends Plugin
+
+  describe("Plugin") {
+    describe("#name") {
+      it("should be the name of the class implementing the plugin") {
+        val expected = classOf[TestPlugin].getName
+
+        val actual = testPlugin.name
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#priority") {
+      it("should return the priority set by the plugin's annotation") {
+        val expected = 999
+
+        val actual = (new PriorityPlugin).priority
+
+        actual should be (expected)
+      }
+
+      it("should default to zero if not set via the plugin's annotation") {
+        val expected = Plugin.DefaultPriority
+
+        val actual = (new TestPlugin).priority
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#initMethods") {
+      it("should return any method annotated with @Init including from ancestors") {
+        val expected = Seq(
+          // Inherited
+          classOf[TestPlugin].getDeclaredMethod("init2"),
+          classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+          // Overridden
+          classOf[ExtendedTestPlugin].getDeclaredMethod("init1"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+          // New
+          classOf[ExtendedTestPlugin].getDeclaredMethod("init4"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+        ).map(PluginMethod.apply(extendedTestPlugin, _: Method))
+
+        val actual = extendedTestPlugin.initMethods
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#destroyMethods") {
+      it("should return any method annotated with @Destroy including from ancestors") {
+        val expected = Seq(
+          // Inherited
+          classOf[TestPlugin].getDeclaredMethod("destroy2"),
+          classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+          // Overridden
+          classOf[ExtendedTestPlugin].getDeclaredMethod("destroy1"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+          // New
+          classOf[ExtendedTestPlugin].getDeclaredMethod("destroy4"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+        ).map(PluginMethod.apply(extendedTestPlugin, _: Method))
+
+        val actual = extendedTestPlugin.destroyMethods
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#eventMethods") {
+      it("should return any method annotated with @Event including from ancestors") {
+        val expected = Seq(
+          // Inherited
+          classOf[TestPlugin].getDeclaredMethod("event2"),
+          classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+          // Overridden
+          classOf[ExtendedTestPlugin].getDeclaredMethod("event1"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+          // New
+          classOf[ExtendedTestPlugin].getDeclaredMethod("event4"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+        ).map(PluginMethod.apply(extendedTestPlugin, _: Method))
+
+        val actual = extendedTestPlugin.eventMethods
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#eventsMethods") {
+      it("should return any method annotated with @Events including from ancestors") {
+        val expected = Seq(
+          // Inherited
+          classOf[TestPlugin].getDeclaredMethod("multiEvent2"),
+          classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+          // Overridden
+          classOf[ExtendedTestPlugin].getDeclaredMethod("multiEvent1"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+          // New
+          classOf[ExtendedTestPlugin].getDeclaredMethod("multiEvent4"),
+          classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+        ).map(PluginMethod.apply(extendedTestPlugin, _: Method))
+
+        val actual = extendedTestPlugin.eventsMethods
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#eventMethodMap") {
+      it("should return a map of event names to their annotated methods") {
+        val expected = Map(
+          "event1" -> Seq(
+            // Inherited
+            classOf[TestPlugin].getDeclaredMethod("event2"),
+
+            // Overridden
+            classOf[ExtendedTestPlugin].getDeclaredMethod("event1"),
+            classOf[ExtendedTestPlugin].getDeclaredMethod("multiEvent1"),
+
+            // New
+            classOf[ExtendedTestPlugin].getDeclaredMethod("event4"),
+            classOf[ExtendedTestPlugin].getDeclaredMethod("multiEvent4")
+          ),
+          "event2" -> Seq(
+            // Inherited
+            classOf[TestPlugin].getDeclaredMethod("multiEvent2"),
+
+            // Overridden
+            classOf[ExtendedTestPlugin].getDeclaredMethod("multiEvent1"),
+
+            // New
+            classOf[ExtendedTestPlugin].getDeclaredMethod("multiEvent4")
+          ),
+          "event3" -> Seq(
+            // Inherited
+            classOf[TestPlugin].getDeclaredMethod("multiEvent2")
+          ),
+          "mixed1" -> Seq(
+            // Inherited
+            classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+            // Overridden
+            classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+            // New
+            classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+          ),
+          "mixed2" -> Seq(
+            // Inherited
+            classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+            // Overridden
+            classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+            // New
+            classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+          ),
+          "mixed3" -> Seq(
+            // Inherited
+            classOf[TestPlugin].getDeclaredMethod("mixed2"),
+
+            // Overridden
+            classOf[ExtendedTestPlugin].getDeclaredMethod("mixed1"),
+
+            // New
+            classOf[ExtendedTestPlugin].getDeclaredMethod("mixed4")
+          )
+        ).mapValues(m => m.map(PluginMethod.apply(extendedTestPlugin, _: Method)))
+
+        val actual = extendedTestPlugin.eventMethodMap
+
+        actual.keys should contain theSameElementsAs (expected.keys)
+        actual.foreach { case (k, v) =>
+          v should contain theSameElementsAs (expected(k))
+        }
+      }
+    }
+
+    describe("#register") {
+      it("should not allow registering a dependency if the plugin manager is not set") {
+        intercept[AssertionError] { registerPlugin.register(new AnyRef) }
+        intercept[AssertionError] { registerPlugin.register("id", new AnyRef) }
+      }
+
+      it("should create a new name for the dependency if not specified") {
+        registerPlugin.pluginManager_=(mockPluginManager)
+
+        val value = new AnyRef
+        val mockDependencyManager = mock[DependencyManager]
+        doNothing().when(mockDependencyManager).add(anyString(), mockEq(value))(any[TypeTag[AnyRef]])
+        doReturn(mockDependencyManager).when(mockPluginManager).dependencyManager
+
+        registerPlugin.register(value)
+      }
+
+      it("should add the dependency using the provided name") {
+        registerPlugin.pluginManager_=(mockPluginManager)
+
+        val name = "some name"
+        val value = new AnyRef
+        val mockDependencyManager = mock[DependencyManager]
+        doNothing().when(mockDependencyManager).add(mockEq(name), mockEq(value))(any[TypeTag[AnyRef]])
+        doReturn(mockDependencyManager).when(mockPluginManager).dependencyManager
+
+        registerPlugin.register(name, value)
+      }
+    }
+  }
+
+  private class TestPlugin extends Plugin {
+    @Init def init1() = {}
+    @Init protected def init2() = {}
+    @Init private def init3() = {}
+    @Event(name = "event1") def event1() = {}
+    @Event(name = "event1") protected def event2() = {}
+    @Event(name = "event1") private def event3() = {}
+    @Events(names = Array("event2", "event3")) def multiEvent1() = {}
+    @Events(names = Array("event2", "event3")) protected def multiEvent2() = {}
+    @Events(names = Array("event2", "event3")) private def multiEvent3() = {}
+    @Destroy def destroy1() = {}
+    @Destroy protected def destroy2() = {}
+    @Destroy private def destroy3() = {}
+
+    @Init
+    @Event(name = "mixed1")
+    @Events(names = Array("mixed2", "mixed3"))
+    @Destroy
+    def mixed1() = {}
+
+    @Init
+    @Event(name = "mixed1")
+    @Events(names = Array("mixed2", "mixed3"))
+    @Destroy
+    protected def mixed2() = {}
+
+    @Init
+    @Event(name = "mixed1")
+    @Events(names = Array("mixed2", "mixed3"))
+    @Destroy
+    private def mixed3() = {}
+  }
+
+  private class ExtendedTestPlugin extends TestPlugin {
+    @Init override def init1() = {}
+    @Event(name = "event1") override def event1() = {}
+    @Events(names = Array("event1", "event2")) override def multiEvent1() = {}
+    @Destroy override def destroy1() = {}
+    @Init
+    @Event(name = "mixed1")
+    @Events(names = Array("mixed2", "mixed3"))
+    @Destroy
+    override def mixed1() = {}
+
+    @Init def init4() = {}
+    @Event(name = "event1") def event4() = {}
+    @Events(names = Array("event1", "event2")) def multiEvent4() = {}
+    @Destroy def destroy4() = {}
+    @Init
+    @Event(name = "mixed1")
+    @Events(names = Array("mixed2", "mixed3"))
+    @Destroy
+    def mixed4() = {}
+  }
+
+  private class RegisterPlugin extends Plugin {
+    override def register[T <: AnyRef : TypeTag](
+      value: T
+    ): Unit = super.register(value)
+    override def register[T <: AnyRef](
+      name: String,
+      value: T
+    )(implicit typeTag: TypeTag[T]): Unit = super.register(name, value)
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencyManagerSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencyManagerSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencyManagerSpec.scala
new file mode 100644
index 0000000..4ee02c3
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencyManagerSpec.scala
@@ -0,0 +1,560 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins.dependencies
+
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+
+class DependencyManagerSpec extends FunSpec with Matchers with OneInstancePerTest {
+  private val dependencyManager = new DependencyManager
+
+  describe("DependencyManager") {
+    describe("#Empty") {
+      it("should return the same dependency manager each time") {
+        val expected = DependencyManager.Empty
+        val actual = DependencyManager.Empty
+
+        actual should be (expected)
+      }
+
+      it("should not add dependencies when the add method is invoked") {
+        val d = DependencyManager.Empty
+
+        d.add(new Object)
+        d.add("id", new Object)
+        d.add(Dependency.fromValue(new Object))
+
+        d.toSeq should be (empty)
+      }
+    }
+
+    describe("#from") {
+      it("should return a new dependency manager using the dependencies") {
+        val expected = Seq(
+          Dependency.fromValue("value1"),
+          Dependency.fromValue("value2")
+        )
+
+        val actual = DependencyManager.from(expected: _*).toSeq
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should throw an exception if two dependencies have the same name") {
+        intercept[IllegalArgumentException] {
+          DependencyManager.from(
+            Dependency.fromValueWithName("name", "value1"),
+            Dependency.fromValueWithName("name", "value2")
+          )
+        }
+      }
+    }
+
+    describe("#merge") {
+      it("should return a new dependency manager with both manager's dependencies") {
+        val expected = Seq(
+          Dependency.fromValue("value1"),
+          Dependency.fromValue("value2"),
+          Dependency.fromValue("value3"),
+          Dependency.fromValue("value4")
+        )
+
+        val dm1 = DependencyManager.from(
+          expected.take(expected.length / 2): _*
+        )
+
+        val dm2 = DependencyManager.from(
+          expected.takeRight(expected.length / 2): _*
+        )
+
+        val actual = dm1.merge(dm2).toSeq
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should overwrite any dependency with the same name from this manager with the other") {
+        val expected = Seq(
+          Dependency.fromValueWithName("name", "value1"),
+          Dependency.fromValue("value2"),
+          Dependency.fromValue("value3"),
+          Dependency.fromValue("value4")
+        )
+
+        val dm1 = DependencyManager.from(
+          Dependency.fromValueWithName("name", "value5")
+        )
+
+        val dm2 = DependencyManager.from(expected: _*)
+
+        val actual = dm1.merge(dm2).toSeq
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#toMap") {
+      it("should return a map of dependency names to dependency values") {
+        val expected = Map(
+          "some name" -> new Object,
+          "some other name" -> new Object
+        )
+
+        expected.foreach { case (k, v) => dependencyManager.add(k, v) }
+
+        val actual = dependencyManager.toMap
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#toSeq") {
+      it("should return a sequence of dependency objects") {
+        val expected = Seq(
+          Dependency.fromValue(new Object),
+          Dependency.fromValue(new Object)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.toSeq
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#add") {
+      it("should generate a dependency name if not provided") {
+        dependencyManager.add(new Object)
+
+        dependencyManager.toSeq.head.name should not be (empty)
+      }
+
+      it("should use the provided name as the dependency's name") {
+        val expected = "some name"
+
+        dependencyManager.add(expected, new Object)
+
+        val actual = dependencyManager.toSeq.head.name
+
+        actual should be (expected)
+      }
+
+      it("should use the provided value for the dependency's value") {
+        val expected = new Object
+
+        dependencyManager.add(expected)
+
+        val actual = dependencyManager.toSeq.head.value
+
+        actual should be (expected)
+      }
+
+      it("should use the reflective type of the value for the dependency's type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = typeOf[Object]
+
+        dependencyManager.add(new Object)
+
+        val actual = dependencyManager.toSeq.head.`type`
+
+        actual should be (expected)
+      }
+
+      it("should add the provided dependency object directly") {
+        val expected = Dependency.fromValue(new Object)
+
+        dependencyManager.add(expected)
+
+        val actual = dependencyManager.toSeq.head
+
+        actual should be (expected)
+      }
+
+      it("should throw an exception if a dependency with the same name already exists") {
+        intercept[IllegalArgumentException] {
+          dependencyManager.add("id", new Object)
+          dependencyManager.add("id", new Object)
+        }
+      }
+    }
+
+    describe("#find") {
+      it("should return Some(Dependency) if found by name") {
+        val expected = Some(Dependency.fromValue(new Object))
+
+        dependencyManager.add(expected.get)
+
+        val actual = dependencyManager.find(expected.get.name)
+
+        actual should be (expected)
+      }
+
+      it("should return None if no dependency with a matching name exists") {
+        val expected = None
+
+        val actual = dependencyManager.find("some name")
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#findByType") {
+      it("should return a collection including of dependencies with the same type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[Object], new Object),
+          Dependency("id2", typeOf[Object], new Object)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.findByType(typeOf[Object])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return a collection including of dependencies with a sub type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new String),
+          Dependency("id2", typeOf[String], new String)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.findByType(typeOf[Object])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return an empty collection if no dependency has the type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Nil
+
+        dependencyManager.add(Dependency("id", typeOf[Object], new Object))
+        dependencyManager.add(Dependency("id2", typeOf[Object], new Object))
+
+        val actual = dependencyManager.findByType(typeOf[String])
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#findByTypeClass") {
+      it("should return a collection including of dependencies with the same class for the type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[Object], new Object),
+          Dependency("id2", typeOf[Object], new Object)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.findByTypeClass(classOf[Object])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return a collection including of dependencies with a sub class for the type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new String),
+          Dependency("id2", typeOf[String], new String)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.findByTypeClass(classOf[Object])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return an empty collection if no dependency has a matching class for its type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Nil
+
+        dependencyManager.add(Dependency("id", typeOf[Object], new Object))
+        dependencyManager.add(Dependency("id2", typeOf[Object], new Object))
+
+        val actual = dependencyManager.findByTypeClass(classOf[String])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      ignore("should throw an exception if the dependency's type class is not found in the provided class' classloader") {
+        import scala.reflect.runtime.universe._
+
+        intercept[ClassNotFoundException] {
+          // TODO: Find some class that is in a different classloader and
+          //       create a dependency from it
+          dependencyManager.add(Dependency("id", typeOf[Object], new Object))
+
+          dependencyManager.findByTypeClass(classOf[Object])
+        }
+      }
+    }
+
+    describe("#findByValueClass") {
+      it("should return a collection including of dependencies with the same class for the value") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[AnyVal], new AnyRef),
+          Dependency("id2", typeOf[AnyVal], new AnyRef)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.findByValueClass(classOf[AnyRef])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return a collection including of dependencies with a sub class for the value") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[AnyVal], new String),
+          Dependency("id2", typeOf[AnyVal], new String)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.findByValueClass(classOf[AnyRef])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return an empty collection if no dependency has a matching class for its value") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Nil
+
+        dependencyManager.add(Dependency("id", typeOf[String], new Object))
+        dependencyManager.add(Dependency("id2", typeOf[String], new Object))
+
+        val actual = dependencyManager.findByValueClass(classOf[String])
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#remove") {
+      it("should remove the dependency with the matching name") {
+        val dSeq = Seq(
+          Dependency.fromValue(new Object),
+          Dependency.fromValue(new Object)
+        )
+
+        val dToRemove = Dependency.fromValue(new Object)
+        dSeq.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        dependencyManager.remove(dToRemove.name)
+
+        val actual = dependencyManager.toSeq
+
+        actual should not contain (dToRemove)
+      }
+
+      it("should return Some(Dependency) representing the removed dependency") {
+        val expected = Some(Dependency.fromValue(new Object))
+
+        dependencyManager.add(expected.get)
+
+        val actual = dependencyManager.remove(expected.get.name)
+
+        actual should be (expected)
+      }
+
+      it("should return None if no dependency was removed") {
+        val expected = None
+
+        val actual = dependencyManager.remove("some name")
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#removeByType") {
+      it("should remove dependencies with the specified type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new AnyRef),
+          Dependency("id2", typeOf[String], new AnyRef)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByType(typeOf[String])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should remove dependencies with a type that is a subtype of the specified type") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new AnyRef),
+          Dependency("id2", typeOf[String], new AnyRef)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByType(typeOf[CharSequence])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return a collection of any removed dependencies") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new AnyRef),
+          Dependency("id2", typeOf[CharSequence], new AnyRef)
+        )
+
+        val all = Seq(
+          Dependency("id3", typeOf[Integer], new AnyRef),
+          Dependency("id4", typeOf[Boolean], new AnyRef)
+        ) ++ expected
+
+        all.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByType(typeOf[CharSequence])
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#removeByTypeClass") {
+      it("should remove dependencies with the specified type class") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new AnyRef),
+          Dependency("id2", typeOf[String], new AnyRef)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByTypeClass(classOf[String])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should remove dependencies with a type that is a subtype of the specified type class") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new AnyRef),
+          Dependency("id2", typeOf[String], new AnyRef)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByTypeClass(classOf[CharSequence])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return a collection of any removed dependencies") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[String], new AnyRef),
+          Dependency("id2", typeOf[CharSequence], new AnyRef)
+        )
+
+        val all = Seq(
+          Dependency("id3", typeOf[Integer], new AnyRef),
+          Dependency("id4", typeOf[Boolean], new AnyRef)
+        ) ++ expected
+
+        all.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByTypeClass(classOf[CharSequence])
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+
+    describe("#removeByValueClass") {
+      it("should remove dependencies with the specified value class") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[AnyRef], new String),
+          Dependency("id2", typeOf[AnyRef], new String)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByValueClass(classOf[String])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should remove dependencies with a type that is a subtype of the specified value class") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[AnyRef], new String),
+          Dependency("id2", typeOf[AnyRef], new String)
+        )
+
+        expected.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByValueClass(classOf[CharSequence])
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should return a collection of any removed dependencies") {
+        import scala.reflect.runtime.universe._
+
+        val expected = Seq(
+          Dependency("id", typeOf[AnyRef], new String),
+          Dependency("id2", typeOf[AnyRef], new CharSequence {
+            override def charAt(i: Int): Char = ???
+
+            override def length(): Int = ???
+
+            override def subSequence(i: Int, i1: Int): CharSequence = ???
+          })
+        )
+
+        val all = Seq(
+          Dependency("id3", typeOf[AnyRef], Int.box(3)),
+          Dependency("id4", typeOf[AnyRef], Boolean.box(true))
+        ) ++ expected
+
+        all.foreach(dependencyManager.add(_: Dependency[_ <: AnyRef]))
+
+        val actual = dependencyManager.removeByValueClass(classOf[CharSequence])
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencySpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencySpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencySpec.scala
new file mode 100644
index 0000000..b434e2a
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/dependencies/DependencySpec.scala
@@ -0,0 +1,133 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins.dependencies
+
+import org.scalatest.{FunSpec, OneInstancePerTest, Matchers}
+
+import scala.tools.nsc.util.ScalaClassLoader.URLClassLoader
+
+class DependencySpec extends FunSpec with Matchers with OneInstancePerTest {
+  import scala.reflect.runtime.universe._
+
+  describe("Dependency") {
+    describe("constructor") {
+      it("should throw illegal argument exception if name is null") {
+        intercept[IllegalArgumentException] {
+          Dependency(null, typeOf[DependencySpec], new Object)
+        }
+      }
+
+      it("should throw illegal argument exception if name is empty") {
+        intercept[IllegalArgumentException] {
+          Dependency("", typeOf[DependencySpec], new Object)
+        }
+      }
+
+      it("should throw illegal argument exception if type is null") {
+        intercept[IllegalArgumentException] {
+          Dependency("id", null, new Object)
+        }
+      }
+
+      it("should throw illegal argument exception if value is null") {
+        intercept[IllegalArgumentException] {
+          Dependency("id", typeOf[DependencySpec], null)
+        }
+      }
+    }
+
+    describe("#typeClass") {
+      it("should return the class found in the class loader that matches the type") {
+        val expected = this.getClass
+
+        val d = Dependency("id", typeOf[DependencySpec], new Object)
+        val actual = d.typeClass(this.getClass.getClassLoader)
+
+        actual should be (expected)
+      }
+
+      it("should throw an exception if no matching class is found in the classloader") {
+        intercept[ClassNotFoundException] {
+          val d = Dependency("id", typeOf[DependencySpec], new Object)
+          d.typeClass(new URLClassLoader(Nil, null))
+        }
+      }
+    }
+
+    describe("#valueClass") {
+      it("should return the class directly from the dependency's value") {
+        val expected = classOf[Object]
+
+        val d = Dependency("id", typeOf[DependencySpec], new Object)
+        val actual = d.valueClass
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#fromValue") {
+      it("should generate a unique name for the dependency") {
+        val d = Dependency.fromValue(new Object)
+
+        // TODO: Stub out UUID method to test id was generated
+        d.name should not be (empty)
+      }
+
+      it("should use the provided value as the dependency's value") {
+        val expected = new Object
+
+        val actual = Dependency.fromValue(expected).value
+
+        actual should be (expected)
+      }
+
+      it("should acquire the reflective type from the provided value") {
+        val expected = typeOf[Object]
+
+        val actual = Dependency.fromValue(new Object).`type`
+
+        actual should be (expected)
+      }
+    }
+
+    describe("#fromValueWithName") {
+      it("should use the provided name as the name for the dependency") {
+        val expected = "some dependency name"
+
+        val actual = Dependency.fromValueWithName(expected, new Object).name
+
+        actual should be (expected)
+      }
+
+      it("should use the provided value as the dependency's value") {
+        val expected = new Object
+
+        val actual = Dependency.fromValueWithName("id", expected).value
+
+        actual should be (expected)
+      }
+
+      it("should acquire the reflective type from the provided value") {
+        val expected = typeOf[Object]
+
+        val actual = Dependency.fromValueWithName("id", new Object).`type`
+
+        actual should be (expected)
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/NotAPlugin.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/NotAPlugin.scala b/plugins/src/test/scala/test/utils/NotAPlugin.scala
new file mode 100644
index 0000000..1e7f3fa
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/NotAPlugin.scala
@@ -0,0 +1,26 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+/**
+ * Class that is not a plugin, but has an empty constructor.
+ *
+ * @note Exists in global space instead of nested in test classes due to the
+ *       fact that Scala creates a non-nullary constructor when a class is
+ *       nested.
+ */
+class NotAPlugin

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/PriorityPlugin.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/PriorityPlugin.scala b/plugins/src/test/scala/test/utils/PriorityPlugin.scala
new file mode 100644
index 0000000..4ac9e30
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/PriorityPlugin.scala
@@ -0,0 +1,45 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package test.utils
+
+import org.apache.toree.plugins.annotations._
+
+import scala.collection.mutable
+
+@Priority(level = -1) class PriorityPlugin extends TestPlugin {
+  @Init @Priority(level = 1)
+  override def initMethod(): mutable.Seq[Any] = super.initMethod()
+
+  @Init def initMethod2() = {}
+
+  @Event(name = "event1") @Priority(level = 1)
+  override def eventMethod(): mutable.Seq[Any] = super.eventMethod()
+
+  @Event(name = "event1") def eventMethod2() = {}
+
+  @Events(names = Array("event2", "event3")) @Priority(level = 1)
+  override def eventsMethod(): mutable.Seq[Any] = super.eventsMethod()
+
+  @Events(names = Array("event2", "event3"))
+  def eventsMethod2() = {}
+
+  @Destroy @Priority(level = 1)
+  override def destroyMethod(): mutable.Seq[Any] = super.destroyMethod()
+
+  @Init def destroyMethod2() = {}
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/RegisteringTestPlugin.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/RegisteringTestPlugin.scala b/plugins/src/test/scala/test/utils/RegisteringTestPlugin.scala
new file mode 100644
index 0000000..5519358
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/RegisteringTestPlugin.scala
@@ -0,0 +1,72 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+import org.apache.toree.plugins.Plugin
+import org.apache.toree.plugins.annotations.{Destroy, Event, Events, Init}
+
+import RegisteringTestPlugin._
+
+/**
+ * Test plugin that registers dependencies.
+ *
+ * @note Exists in global space instead of nested in test classes due to the
+ *       fact that Scala creates a non-nullary constructor when a class is
+ *       nested.
+ */
+class RegisteringTestPlugin extends Plugin {
+  type Callback = () => Any
+  private var initCallbacks = collection.mutable.Seq[Callback]()
+  private var eventCallbacks = collection.mutable.Seq[Callback]()
+  private var eventsCallbacks = collection.mutable.Seq[Callback]()
+  private var destroyCallbacks = collection.mutable.Seq[Callback]()
+
+  def addInitCallback(callback: Callback) = initCallbacks :+= callback
+  def addEventCallback(callback: Callback) = eventCallbacks :+= callback
+  def addEventsCallback(callback: Callback) = eventsCallbacks :+= callback
+  def addDestroyCallback(callback: Callback) = destroyCallbacks :+= callback
+
+  @Init def initMethod() = {
+    register(InitDepName, TestPluginDependency(996))
+    initCallbacks.map(_())
+  }
+
+  @Event(name = "event1") def eventMethod() = {
+    register(EventDepName, TestPluginDependency(997))
+    eventCallbacks.map(_())
+  }
+
+  @Events(names = Array("event2", "event3")) def eventsMethod() = {
+    register(EventsDepName, TestPluginDependency(998))
+    eventsCallbacks.map(_ ())
+  }
+
+  @Destroy def destroyMethod() = {
+    register(DestroyDepName, TestPluginDependency(999))
+    destroyCallbacks.map(_())
+  }
+}
+
+object RegisteringTestPlugin {
+  val DefaultEvent = "event1"
+  val DefaultEvents1 = "event2"
+  val DefaultEvents2 = "event3"
+  val InitDepName = "init-dep"
+  val EventDepName = "event-dep"
+  val EventsDepName = "events-dep"
+  val DestroyDepName = "destroy-dep"
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/TestClassInfo.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/TestClassInfo.scala b/plugins/src/test/scala/test/utils/TestClassInfo.scala
new file mode 100644
index 0000000..ab59802
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/TestClassInfo.scala
@@ -0,0 +1,34 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+import java.io.File
+
+import org.clapper.classutil.Modifier.Modifier
+import org.clapper.classutil.{ClassInfo, FieldInfo, MethodInfo}
+
+case class TestClassInfo(
+  superClassName: String = "",
+  interfaces: List[String] = Nil,
+  location: File = null,
+  methods: Set[MethodInfo] = Set(),
+  fields: Set[FieldInfo] = Set(),
+  signature: String = "",
+  modifiers: Set[Modifier] = Set(),
+  name: String = ""
+) extends ClassInfo
+

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/TestFieldInfo.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/TestFieldInfo.scala b/plugins/src/test/scala/test/utils/TestFieldInfo.scala
new file mode 100644
index 0000000..492fc94
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/TestFieldInfo.scala
@@ -0,0 +1,30 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+import org.clapper.classutil.FieldInfo
+import org.clapper.classutil.Modifier.Modifier
+
+case class TestFieldInfo(
+  signature: String = "",
+  descriptor: String = "",
+  exceptions: List[String] = Nil,
+  modifiers: Set[Modifier] = Set(),
+  name: String = "",
+  value: Option[Object] = None
+) extends FieldInfo
+

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/test/utils/TestMethodInfo.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/test/utils/TestMethodInfo.scala b/plugins/src/test/scala/test/utils/TestMethodInfo.scala
new file mode 100644
index 0000000..7e61cc7
--- /dev/null
+++ b/plugins/src/test/scala/test/utils/TestMethodInfo.scala
@@ -0,0 +1,29 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package test.utils
+
+import org.clapper.classutil.MethodInfo
+import org.clapper.classutil.Modifier.Modifier
+
+private case class TestMethodInfo(
+  signature: String = "",
+  descriptor: String = "",
+  exceptions: List[String] = Nil,
+  modifiers: Set[Modifier] = Set(),
+  name: String = ""
+) extends MethodInfo
+



[3/3] incubator-toree git commit: Added initial plugin implementation

Posted by lb...@apache.org.
Added initial plugin implementation


Project: http://git-wip-us.apache.org/repos/asf/incubator-toree/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-toree/commit/4c0dccfb
Tree: http://git-wip-us.apache.org/repos/asf/incubator-toree/tree/4c0dccfb
Diff: http://git-wip-us.apache.org/repos/asf/incubator-toree/diff/4c0dccfb

Branch: refs/heads/master
Commit: 4c0dccfb795f4ef3c6ccde451fbace39e7238c08
Parents: 085c297
Author: Chip Senkbeil <ch...@gatech.edu>
Authored: Tue Feb 23 20:20:30 2016 -0600
Committer: Chip Senkbeil <ch...@gmail.com>
Committed: Mon Feb 29 08:05:37 2016 -0600

----------------------------------------------------------------------
 .../CoursierDependencyDownloader.scala          |  16 +
 .../org/apache/toree/annotations/Internal.scala |  28 +
 plugins/build.sbt                               |  22 +
 .../toree/plugins/annotations/DepName.java      |  14 +
 .../toree/plugins/annotations/Destroy.java      |  12 +
 .../apache/toree/plugins/annotations/Event.java |  14 +
 .../toree/plugins/annotations/Events.java       |  14 +
 .../apache/toree/plugins/annotations/Init.java  |  12 +
 .../toree/plugins/annotations/Priority.java     |  14 +
 .../org/apache/toree/plugins/Implicits.scala    |  34 ++
 .../scala/org/apache/toree/plugins/Plugin.scala | 115 ++++
 .../toree/plugins/PluginClassLoader.scala       |  42 ++
 .../apache/toree/plugins/PluginManager.scala    | 367 ++++++++++++
 .../org/apache/toree/plugins/PluginMethod.scala | 131 +++++
 .../toree/plugins/PluginMethodResult.scala      |  75 +++
 .../apache/toree/plugins/PluginSearcher.scala   | 106 ++++
 .../plugins/UnknownPluginTypeException.scala    |  26 +
 .../toree/plugins/dependencies/Dependency.scala |  80 +++
 .../dependencies/DependencyException.scala      |  56 ++
 .../dependencies/DependencyManager.scala        | 197 +++++++
 .../PluginManagerSpecForIntegration.scala       |  76 +++
 .../apache/toree/plugins/ImplicitsSpec.scala    |  54 ++
 .../toree/plugins/PluginClassLoaderSpec.scala   |  55 ++
 .../toree/plugins/PluginManagerSpec.scala       | 532 ++++++++++++++++++
 .../toree/plugins/PluginMethodResultSpec.scala  | 151 +++++
 .../apache/toree/plugins/PluginMethodSpec.scala | 319 +++++++++++
 .../toree/plugins/PluginSearcherSpec.scala      | 184 ++++++
 .../org/apache/toree/plugins/PluginSpec.scala   | 327 +++++++++++
 .../dependencies/DependencyManagerSpec.scala    | 560 +++++++++++++++++++
 .../plugins/dependencies/DependencySpec.scala   | 133 +++++
 .../src/test/scala/test/utils/NotAPlugin.scala  |  26 +
 .../test/scala/test/utils/PriorityPlugin.scala  |  45 ++
 .../test/utils/RegisteringTestPlugin.scala      |  72 +++
 .../test/scala/test/utils/TestClassInfo.scala   |  34 ++
 .../test/scala/test/utils/TestFieldInfo.scala   |  30 +
 .../test/scala/test/utils/TestMethodInfo.scala  |  29 +
 .../src/test/scala/test/utils/TestPlugin.scala  |  52 ++
 .../test/utils/TestPluginWithDependencies.scala |  59 ++
 project/Build.scala                             |  22 +-
 project/Common.scala                            |   3 +-
 40 files changed, 4135 insertions(+), 3 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/kernel-api/src/main/scala/org/apache/toree/dependencies/CoursierDependencyDownloader.scala
----------------------------------------------------------------------
diff --git a/kernel-api/src/main/scala/org/apache/toree/dependencies/CoursierDependencyDownloader.scala b/kernel-api/src/main/scala/org/apache/toree/dependencies/CoursierDependencyDownloader.scala
index 925ed0b..ef9ac02 100644
--- a/kernel-api/src/main/scala/org/apache/toree/dependencies/CoursierDependencyDownloader.scala
+++ b/kernel-api/src/main/scala/org/apache/toree/dependencies/CoursierDependencyDownloader.scala
@@ -1,3 +1,19 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
 package org.apache.toree.dependencies
 
 import java.io.{File, PrintStream}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/macros/src/main/scala/org/apache/toree/annotations/Internal.scala
----------------------------------------------------------------------
diff --git a/macros/src/main/scala/org/apache/toree/annotations/Internal.scala b/macros/src/main/scala/org/apache/toree/annotations/Internal.scala
new file mode 100644
index 0000000..a1ada93
--- /dev/null
+++ b/macros/src/main/scala/org/apache/toree/annotations/Internal.scala
@@ -0,0 +1,28 @@
+
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package org.apache.toree.annotations
+
+import scala.annotation.{Annotation, StaticAnnotation}
+import scala.language.experimental.macros
+
+/**
+ * Marks as internal, indicating that the API should not be treated as a stable,
+ * public API.
+ */
+class Internal extends Annotation with StaticAnnotation

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/build.sbt
----------------------------------------------------------------------
diff --git a/plugins/build.sbt b/plugins/build.sbt
new file mode 100644
index 0000000..4ae25c3
--- /dev/null
+++ b/plugins/build.sbt
@@ -0,0 +1,22 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+// BSD 3-clause license, used for detecting plugins
+libraryDependencies += "org.clapper" %% "classutil" % "1.0.3"
+
+// Needed for type inspection
+libraryDependencies += "org.scala-lang" % "scala-reflect" % scalaVersion.value

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/java/org/apache/toree/plugins/annotations/DepName.java
----------------------------------------------------------------------
diff --git a/plugins/src/main/java/org/apache/toree/plugins/annotations/DepName.java b/plugins/src/main/java/org/apache/toree/plugins/annotations/DepName.java
new file mode 100644
index 0000000..811ac02
--- /dev/null
+++ b/plugins/src/main/java/org/apache/toree/plugins/annotations/DepName.java
@@ -0,0 +1,14 @@
+package org.apache.toree.plugins.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Represents a marker for loading a dependency for a specific name.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.PARAMETER })
+public @interface DepName {
+    String name();
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/java/org/apache/toree/plugins/annotations/Destroy.java
----------------------------------------------------------------------
diff --git a/plugins/src/main/java/org/apache/toree/plugins/annotations/Destroy.java b/plugins/src/main/java/org/apache/toree/plugins/annotations/Destroy.java
new file mode 100644
index 0000000..556624d
--- /dev/null
+++ b/plugins/src/main/java/org/apache/toree/plugins/annotations/Destroy.java
@@ -0,0 +1,12 @@
+package org.apache.toree.plugins.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Represents a marker for a plugin shutdown method.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface Destroy {}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/java/org/apache/toree/plugins/annotations/Event.java
----------------------------------------------------------------------
diff --git a/plugins/src/main/java/org/apache/toree/plugins/annotations/Event.java b/plugins/src/main/java/org/apache/toree/plugins/annotations/Event.java
new file mode 100644
index 0000000..7318a8b
--- /dev/null
+++ b/plugins/src/main/java/org/apache/toree/plugins/annotations/Event.java
@@ -0,0 +1,14 @@
+package org.apache.toree.plugins.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Represents a marker for a generic plugin event.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface Event {
+    String name();
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/java/org/apache/toree/plugins/annotations/Events.java
----------------------------------------------------------------------
diff --git a/plugins/src/main/java/org/apache/toree/plugins/annotations/Events.java b/plugins/src/main/java/org/apache/toree/plugins/annotations/Events.java
new file mode 100644
index 0000000..210e07d
--- /dev/null
+++ b/plugins/src/main/java/org/apache/toree/plugins/annotations/Events.java
@@ -0,0 +1,14 @@
+package org.apache.toree.plugins.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Represents a marker for multiple generic plugin events.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface Events {
+    String[] names();
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/java/org/apache/toree/plugins/annotations/Init.java
----------------------------------------------------------------------
diff --git a/plugins/src/main/java/org/apache/toree/plugins/annotations/Init.java b/plugins/src/main/java/org/apache/toree/plugins/annotations/Init.java
new file mode 100644
index 0000000..920bd1d
--- /dev/null
+++ b/plugins/src/main/java/org/apache/toree/plugins/annotations/Init.java
@@ -0,0 +1,12 @@
+package org.apache.toree.plugins.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Represents a marker for a plugin initialization method.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.METHOD })
+public @interface Init {}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/java/org/apache/toree/plugins/annotations/Priority.java
----------------------------------------------------------------------
diff --git a/plugins/src/main/java/org/apache/toree/plugins/annotations/Priority.java b/plugins/src/main/java/org/apache/toree/plugins/annotations/Priority.java
new file mode 100644
index 0000000..39145ac
--- /dev/null
+++ b/plugins/src/main/java/org/apache/toree/plugins/annotations/Priority.java
@@ -0,0 +1,14 @@
+package org.apache.toree.plugins.annotations;
+
+import java.lang.annotation.*;
+
+/**
+ * Represents an indicator of priority for plugins and plugin methods.
+ */
+@Documented
+@Inherited
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ ElementType.TYPE, ElementType.METHOD })
+public @interface Priority {
+    long level();
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/Implicits.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/Implicits.scala b/plugins/src/main/scala/org/apache/toree/plugins/Implicits.scala
new file mode 100644
index 0000000..bd2e456
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/Implicits.scala
@@ -0,0 +1,34 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import org.apache.toree.plugins.dependencies.Dependency
+
+import scala.reflect.runtime.universe.TypeTag
+
+/**
+ * Contains plugin implicit methods.
+ */
+object Implicits {
+  import scala.language.implicitConversions
+
+  implicit def $dep[T <: AnyRef : TypeTag](bundle: (String, T)): Dependency[T] =
+    Dependency.fromValueWithName(bundle._1, bundle._2)
+
+  implicit def $dep[T <: AnyRef : TypeTag](value: T): Dependency[T] =
+    Dependency.fromValue(value)
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/Plugin.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/Plugin.scala b/plugins/src/main/scala/org/apache/toree/plugins/Plugin.scala
new file mode 100644
index 0000000..66cd4a8
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/Plugin.scala
@@ -0,0 +1,115 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.lang.reflect.Method
+import java.util.concurrent.ConcurrentHashMap
+
+import org.apache.toree.annotations.Internal
+
+import scala.reflect.runtime.universe.TypeTag
+
+/**
+ * Contains constants for the plugin interface.
+ */
+object Plugin {
+  /** Default priority for a plugin if not marked explicitly. */
+  val DefaultPriority: Long = 0
+}
+
+/**
+ * Represents the generic plugin interface.
+ */
+trait Plugin {
+  /** Plugin manager containing the plugin */
+  @Internal private var _pluginManager: PluginManager = null
+
+  /** Represents the name of the plugin. */
+  final val name: String = getClass.getName
+
+  /** Represents the priority of the plugin. */
+  final val priority: Long = {
+    Option(getClass.getAnnotation(classOf[annotations.Priority]))
+      .map(_.level())
+      .getOrElse(Plugin.DefaultPriority)
+  }
+
+  /** Sets the plugin manager pointer for this plugin. */
+  @Internal private[plugins] final def pluginManager_=(_pluginManager: PluginManager) = {
+    require(this._pluginManager == null, "Plugin manager cannot be reassigned!")
+    this._pluginManager = _pluginManager
+  }
+
+  /** Returns the plugin manager pointer for this plugin. */
+  @Internal private[plugins] final def pluginManager = _pluginManager
+
+  /** Represents all @init methods in the plugin. */
+  @Internal private[plugins] final lazy val initMethods: Seq[PluginMethod] = {
+    allMethods.filter(_.isInit)
+  }
+
+  /** Represents all @destroy methods in the plugin. */
+  @Internal private[plugins] final lazy val destroyMethods: Seq[PluginMethod] = {
+    allMethods.filter(_.isDestroy)
+  }
+
+  /** Represents all @event methods in the plugin. */
+  @Internal private[plugins] final lazy val eventMethods: Seq[PluginMethod] = {
+    allMethods.filter(_.isEvent)
+  }
+
+  /** Represents all @events methods in the plugin. */
+  @Internal private[plugins] final lazy val eventsMethods: Seq[PluginMethod] = {
+    allMethods.filter(_.isEvents)
+  }
+
+  /** Represents all public/protected methods contained by this plugin. */
+  private final lazy val allMethods: Seq[PluginMethod] =
+    getClass.getMethods.map(PluginMethod.apply(this, _: Method))
+
+  /** Represents mapping of event names to associated plugin methods. */
+  @Internal private[plugins] final lazy val eventMethodMap: Map[String, Seq[PluginMethod]] = {
+    val allEventMethods = (eventMethods ++ eventsMethods).distinct
+    val allEventNames = allEventMethods.flatMap(_.eventNames).distinct
+    allEventNames.map(name =>
+      name -> allEventMethods.filter(_.eventNames.contains(name))
+    ).toMap
+  }
+
+  /**
+   * Registers a new dependency to be associated with this plugin.
+   *
+   * @param value The value of the dependency
+   * @tparam T The dependency's type
+   */
+  protected def register[T <: AnyRef : TypeTag](value: T): Unit = {
+    register(java.util.UUID.randomUUID().toString, value)
+  }
+
+  /**
+   * Registers a new dependency to be associated with this plugin.
+   *
+   * @param name The name of the dependency
+   * @param value The value of the dependency
+   * @param typeTag The type information for the dependency
+   * @tparam T The dependency's type
+   */
+  protected def register[T <: AnyRef](name: String, value: T)(implicit typeTag: TypeTag[T]): Unit = {
+    assert(_pluginManager != null, "Internal plugin manager reference invalid!")
+    _pluginManager.dependencyManager.add(name, value)
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/PluginClassLoader.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/PluginClassLoader.scala b/plugins/src/main/scala/org/apache/toree/plugins/PluginClassLoader.scala
new file mode 100644
index 0000000..2a9b295
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/PluginClassLoader.scala
@@ -0,0 +1,42 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.net.{URLClassLoader, URL}
+
+/**
+ * Represents a class loader used to manage classes used as plugins.
+ *
+ * @param urls The initial collection of URLs pointing to paths to load
+ *             plugin classes
+ * @param parentLoader The parent loader to use as a fallback to load plugin
+ *                     classes
+ */
+class PluginClassLoader(
+  private val urls: Seq[URL],
+  private val parentLoader: ClassLoader
+) extends URLClassLoader(urls.toArray, parentLoader) {
+  /**
+   * Adds a new URL to be included when loading plugins. If the url is already
+   * in the class loader, it is ignored.
+   *
+   * @param url The url pointing to the new plugin classes to load
+   */
+  override def addURL(url: URL): Unit = {
+    if (!this.getURLs.contains(url)) super.addURL(url)
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/PluginManager.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/PluginManager.scala b/plugins/src/main/scala/org/apache/toree/plugins/PluginManager.scala
new file mode 100644
index 0000000..4b7338f
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/PluginManager.scala
@@ -0,0 +1,367 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.io.File
+import java.util.concurrent.ConcurrentHashMap
+import org.apache.toree.plugins.dependencies._
+import org.slf4j.LoggerFactory
+
+import scala.collection.JavaConverters._
+import scala.util.{Failure, Success, Try}
+
+/**
+ * Represents a manager of plugins to be loaded/executed/unloaded.
+ *
+ * @param pluginClassLoader The main classloader for loading plugins
+ * @param pluginSearcher The search utility to find plugin classes
+ * @param dependencyManager The dependency manager for plugins
+ */
+class PluginManager(
+  private val pluginClassLoader: PluginClassLoader =
+    new PluginClassLoader(Nil, classOf[PluginManager].getClassLoader),
+  private val pluginSearcher: PluginSearcher = new PluginSearcher,
+  val dependencyManager: DependencyManager = new DependencyManager
+) {
+  /** Represents logger used by plugin manager. */
+  private val logger = LoggerFactory.getLogger(this.getClass)
+
+  /** Represents internal plugins. */
+  private lazy val internalPlugins: Map[String, Class[_]] =
+    pluginSearcher.internal
+      .map(_.name)
+      .map(n => n -> pluginClassLoader.loadClass(n))
+      .toMap
+
+  /** Represents external plugins that can be loaded/unloaded. */
+  private lazy val externalPlugins: collection.mutable.Map[String, Class[_]] =
+    new ConcurrentHashMap[String, Class[_]]().asScala
+
+  /** Represents all active (loaded and created) plugins. */
+  private lazy val activePlugins: collection.mutable.Map[String, Plugin] =
+    new ConcurrentHashMap[String, Plugin]().asScala
+
+  /**
+   * Returns whether or not the specified plugin is active.
+   *
+   * @param name The fully-qualified name of the plugin class
+   * @return True if actively loaded, otherwise false
+   */
+  def isActive(name: String): Boolean = activePlugins.contains(name)
+
+  /**
+   * Returns a new iterator over active plugins contained by this manager.
+   *
+   * @return The iterator of active plugins
+   */
+  def plugins: Iterable[Plugin] = activePlugins.values
+
+  /**
+   * Initializes the plugin manager, performing the expensive task of searching
+   * for all internal plugins, creating them, and initializing them.
+   *
+   * @return The collection of loaded plugins
+   */
+  def initialize(): Seq[Plugin] = {
+    val newPlugins = internalPlugins.flatMap(t =>
+      loadPlugin(t._1, t._2).toOption
+    ).toSeq
+    initializePlugins(newPlugins, DependencyManager.Empty)
+    newPlugins
+  }
+
+  /**
+   * Loads (but does not initialize) plugins from the provided paths.
+   *
+   * @param paths The file paths from which to load new plugins
+   * @return The collection of loaded plugins
+   */
+  def loadPlugins(paths: File*): Seq[Plugin] = {
+    // Search for plugins in our new paths, then add loaded plugins to list
+    // NOTE: Iterator returned from plugin searcher, so avoid building a
+    //       large collection by performing all tasks together
+    @volatile var newPlugins = collection.mutable.Seq[Plugin]()
+    pluginSearcher.search(paths: _*).foreach(ci => {
+      // Add valid path to class loader
+      pluginClassLoader.addURL(ci.location.toURI.toURL)
+
+      // Load class
+      val klass = pluginClassLoader.loadClass(ci.name)
+
+      // Add to external plugin list
+      externalPlugins.put(ci.name, klass)
+
+      // Load the plugin using the given name and class
+      loadPlugin(ci.name, klass).foreach(newPlugins :+= _)
+    })
+    newPlugins
+  }
+
+  /**
+   * Loads the plugin using the specified name.
+   *
+   * @param name The name of the plugin
+   * @param klass The class representing the plugin
+   * @return The new plugin instance if no plugin with the specified name
+   *         exists, otherwise the plugin instance with the name
+   */
+  def loadPlugin(name: String, klass: Class[_]): Try[Plugin] = {
+    if (isActive(name)) {
+      logger.warn(s"Skipping $name as already actively loaded!")
+      Success(activePlugins(name))
+    } else {
+      logger.debug(s"Loading $name as plugin")
+
+      // Assume that each plugin has an empty constructor
+      val tryInstance = Try(klass.newInstance())
+
+      // Log failures
+      tryInstance.failed.foreach(ex =>
+        logger.error(s"Failed to load plugin $name", ex))
+
+      // Attempt to cast as plugin type to add to active plugins
+      tryInstance.transform({
+        case p: Plugin  =>
+          p.pluginManager_=(this)
+          activePlugins.put(p.name, p)
+          Success(p)
+        case x          =>
+          val name = x.getClass.getName
+          logger.warn(s"Unknown plugin type '$name', ignoring!")
+          Failure(new UnknownPluginTypeException(name))
+      }, f => Failure(f))
+    }
+  }
+
+  /**
+   * Initializes a collection of plugins that may/may not have
+   * dependencies on one another.
+   *
+   * @param plugins The collection of plugins to initialize
+   * @param scopedDependencyManager The dependency manager containing scoped
+   *                                dependencies to use over global ones
+   * @return The collection of results in order of priority (higher to lower)
+   */
+  def initializePlugins(
+    plugins: Seq[Plugin],
+    scopedDependencyManager: DependencyManager = DependencyManager.Empty
+  ): Seq[PluginMethodResult] = {
+    val pluginMethods = plugins.flatMap(_.initMethods)
+    val results = invokePluginMethods(
+      pluginMethods,
+      scopedDependencyManager
+    )
+
+    // Mark success/failure
+    results.groupBy(_.pluginName).foreach { case (pluginName, g) =>
+      val failures = g.flatMap(_.toTry.failed.toOption)
+      val success = failures.isEmpty
+
+      if (success) logger.debug(s"Successfully initialized plugin $pluginName!")
+      else logger.warn(s"Initialization failed for plugin $pluginName!")
+
+      // Log any specific failures for the plugin
+      failures.foreach(ex => logger.error(pluginName, ex))
+    }
+
+    results
+  }
+
+  /**
+   * Destroys a collection of plugins that may/may not have
+   * dependencies on one another.
+   *
+   * @param plugins The collection of plugins to destroy
+   * @param scopedDependencyManager The dependency manager containing scoped
+   *                                dependencies to use over global ones
+   * @param destroyOnFailure If true, destroys the plugin even if its destroy
+   *                         callback fails
+   * @return The collection of results in order of priority (higher to lower)
+   */
+  def destroyPlugins(
+    plugins: Seq[Plugin],
+    scopedDependencyManager: DependencyManager = DependencyManager.Empty,
+    destroyOnFailure: Boolean = true
+  ): Seq[PluginMethodResult] = {
+    val pluginMethods = plugins.flatMap(_.destroyMethods)
+    val results = invokePluginMethods(
+      pluginMethods,
+      scopedDependencyManager
+    )
+
+    // Perform check to remove destroyed plugins
+    results.groupBy(_.pluginName).foreach { case (pluginName, g) =>
+      val failures = g.flatMap(_.toTry.failed.toOption)
+      val success = failures.isEmpty
+
+      if (success) logger.debug(s"Successfully destroyed plugin $pluginName!")
+      else if (destroyOnFailure) logger.debug(
+        s"Failed to invoke some teardown methods, but destroyed plugin $pluginName!"
+      )
+      else logger.warn(s"Failed to destroy plugin $pluginName!")
+
+      // If successful or forced, remove the plugin from our active list
+      if (success || destroyOnFailure) activePlugins.remove(pluginName)
+
+      // Log any specific failures for the plugin
+      failures.foreach(ex => logger.error(pluginName, ex))
+    }
+
+    results
+  }
+
+  /**
+   * Finds a plugin with the matching name.
+   *
+   * @param name The fully-qualified class name of the plugin
+   * @return Some plugin if found, otherwise None
+   */
+  def findPlugin(name: String): Option[Plugin] = plugins.find(_.name == name)
+
+  /**
+   * Sends an event to all plugins actively listening for that event and
+   * returns the first result, which is based on highest priority.
+   *
+   * @param eventName The name of the event
+   * @param scopedDependencies The dependencies to provide directly to event
+   *                           handlers
+   * @return The first result from all plugin methods that executed the event
+   */
+  def fireEventFirstResult(
+    eventName: String,
+    scopedDependencies: Dependency[_ <: AnyRef]*
+  ): Option[PluginMethodResult] = {
+    fireEvent(eventName, scopedDependencies: _*).headOption
+  }
+
+  /**
+   * Sends an event to all plugins actively listening for that event and
+   * returns the last result, which is based on highest priority.
+   *
+   * @param eventName The name of the event
+   * @param scopedDependencies The dependencies to provide directly to event
+   *                           handlers
+   * @return The last result from all plugin methods that executed the event
+   */
+  def fireEventLastResult(
+    eventName: String,
+    scopedDependencies: Dependency[_ <: AnyRef]*
+  ): Option[PluginMethodResult] = {
+    fireEvent(eventName, scopedDependencies: _*).lastOption
+  }
+
+  /**
+   * Sends an event to all plugins actively listening for that event.
+   *
+   * @param eventName The name of the event
+   * @param scopedDependencies The dependencies to provide directly to event
+   *                           handlers
+   * @return The collection of results in order of priority (higher to lower)
+   */
+  def fireEvent(
+    eventName: String,
+    scopedDependencies: Dependency[_ <: AnyRef]*
+  ): Seq[PluginMethodResult] = {
+    val dependencyManager = new DependencyManager
+    scopedDependencies.foreach(d => dependencyManager.add(d))
+    fireEvent(eventName, dependencyManager)
+  }
+
+  /**
+   * Sends an event to all plugins actively listening for that event.
+   *
+   * @param eventName The name of the event
+   * @param scopedDependencyManager The dependency manager containing scoped
+   *                                dependencies to use over global ones
+   * @return The collection of results in order of priority (higher to lower)
+   */
+  def fireEvent(
+    eventName: String,
+    scopedDependencyManager: DependencyManager = DependencyManager.Empty
+  ): Seq[PluginMethodResult] = {
+    val methods = plugins.flatMap(_.eventMethodMap.getOrElse(eventName, Nil))
+
+    invokePluginMethods(methods.toSeq, scopedDependencyManager)
+  }
+
+  /**
+   * Attempts to invoke all provided plugin methods. This is a naive
+   * implementation that continually invokes bundles until either all bundles
+   * are complete or failures are detected (needing dependencies that other
+   * bundles do not provide).
+   *
+   * @param pluginMethods The collection of plugin methods to invoke
+   * @param scopedDependencyManager The dependency manager containing scoped
+   *                                dependencies to use over global ones
+   * @return The collection of results in order of priority
+   */
+  private def invokePluginMethods(
+    pluginMethods: Seq[PluginMethod],
+    scopedDependencyManager: DependencyManager
+  ): Seq[PluginMethodResult] = {
+    // Continue trying to invoke plugins until we finish them all or
+    // we reach a state where no plugin can be completed
+    val completedMethods = Array.ofDim[PluginMethodResult](pluginMethods.size)
+
+    // Sort by method priority and then, for ties, plugin priority
+    @volatile var remainingMethods = prioritizePluginMethods(pluginMethods)
+
+    @volatile var done = false
+    while (!done) {
+      // NOTE: Performing this per iteration as the global dependency manager
+      //       can be updated by plugins with each invocation
+      val dm = dependencyManager.merge(scopedDependencyManager)
+
+      // Process all methods, adding any successful to completed and leaving
+      // any failures to be processed again
+      val newRemainingMethods = remainingMethods.map { case (m, i) =>
+        val result = m.invoke(dm)
+        if (result.isSuccess) completedMethods.update(i, result)
+        (m, i, result)
+      }.filter(_._3.isFailure)
+
+      // If no change detected, we have failed to process all methods
+      if (remainingMethods.size == newRemainingMethods.size) {
+        // Place last failure for each method in our completed list
+        newRemainingMethods.foreach { case (_, i, r) =>
+          completedMethods.update(i, r)
+        }
+        done = true
+      } else {
+        // Update remaining methods to past failures
+        remainingMethods = newRemainingMethods.map(t => (t._1, t._2))
+        done = remainingMethods.isEmpty
+      }
+    }
+
+    completedMethods
+  }
+
+  /**
+   * Sorts plugin methods based on method priority and plugin priority.
+   *
+   * @param pluginMethods The collection of plugin methods to sort
+   * @return The sorted plugin methods
+   */
+  private def prioritizePluginMethods(pluginMethods: Seq[PluginMethod]) =
+    pluginMethods
+      .groupBy(_.priority)
+      .flatMap(_._2.sortWith(_.plugin.priority > _.plugin.priority))
+      .toSeq
+      .sortWith(_.priority > _.priority)
+      .zipWithIndex
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/PluginMethod.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/PluginMethod.scala b/plugins/src/main/scala/org/apache/toree/plugins/PluginMethod.scala
new file mode 100644
index 0000000..cb87b65
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/PluginMethod.scala
@@ -0,0 +1,131 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.lang.reflect.{InvocationTargetException, Method}
+
+import org.apache.toree.plugins.annotations._
+import org.apache.toree.plugins.dependencies._
+
+import scala.util.Try
+
+/**
+ * Represents a method for a specific plugin
+ *
+ * @param plugin The plugin containing this method
+ * @param method The method to invoke
+ */
+case class PluginMethod(
+  plugin: Plugin,
+  method: Method
+) {
+  /** Represents the collection of names of events this method supports. */
+  lazy val eventNames: Seq[String] = {
+    Option(method.getAnnotation(classOf[Event]))
+      .map(_.name()).map(Seq(_)).getOrElse(Nil) ++
+    Option(method.getAnnotation(classOf[Events]))
+      .map(_.names()).map(_.toSeq).getOrElse(Nil)
+  }
+
+  /** Represents whether or not this method triggers on initialization. */
+  lazy val isInit: Boolean = method.isAnnotationPresent(classOf[Init])
+
+  /** Represents whether or not this method contains an Event annotation. */
+  lazy val isEvent: Boolean = method.isAnnotationPresent(classOf[Event])
+
+  /** Represents whether or not this method contains an Events annotation. */
+  lazy val isEvents: Boolean = method.isAnnotationPresent(classOf[Events])
+
+  /** Represents whether or not this method triggers on destruction. */
+  lazy val isDestroy: Boolean = method.isAnnotationPresent(classOf[Destroy])
+
+  /** Represents this method's priority. */
+  lazy val priority: Long = Option(method.getAnnotation(classOf[Priority]))
+    .map(_.level()).getOrElse(PluginMethod.DefaultPriority)
+
+  /**
+   * Invokes by loading all needed dependencies and providing them as
+   * arguments to the method.
+   *
+   * @param dependencies The collection of dependencies to inject into the
+   *                     method for its arguments (as needed)
+   * @return The result from invoking the plugin
+   */
+  @throws[DepNameNotFoundException]
+  @throws[DepClassNotFoundException]
+  @throws[DepUnexpectedClassException]
+  def invoke(dependencies: Dependency[_ <: AnyRef]*): PluginMethodResult = {
+    invoke(DependencyManager.from(dependencies: _*))
+  }
+
+  /**
+   * Invokes by loading all needed dependencies and providing them as
+   * arguments to the method.
+   *
+   * @param dependencyManager The dependency manager containing dependencies
+   *                          to inject into the method for its arguments
+   *                          (as needed)
+   * @return The result from invoking the plugin
+   */
+  @throws[DepNameNotFoundException]
+  @throws[DepClassNotFoundException]
+  @throws[DepUnexpectedClassException]
+  def invoke(dependencyManager: DependencyManager): PluginMethodResult = Try({
+    // Get dependency info (if has specific name or just use class)
+    val depInfo = method.getParameterAnnotations
+      .zip(method.getParameterTypes)
+      .map { case (annotations, parameterType) =>
+        (annotations.collect {
+          case dn: DepName => dn
+        }.lastOption.map(_.name()), parameterType)
+      }
+
+    // Load dependencies for plugin method
+    val dependencies = depInfo.map { case (name, c) => name match {
+      case Some(n) =>
+        val dep = dependencyManager.find(n)
+        if (dep.isEmpty) throw new DepNameNotFoundException(n)
+
+        // Verify found dep has acceptable class
+        val depClass: Class[_] = dep.get.valueClass
+        if (!c.isAssignableFrom(depClass))
+          throw new DepUnexpectedClassException(n, c, depClass)
+
+        dep.get
+      case None =>
+        val deps = dependencyManager.findByValueClass(c)
+        if (deps.isEmpty) throw new DepClassNotFoundException(c)
+        deps.last
+    } }
+
+    // Validate arguments
+    val arguments: Seq[AnyRef] = dependencies.map(_.value.asInstanceOf[AnyRef])
+
+    // Invoke plugin method
+    method.invoke(plugin, arguments: _*)
+  }).map(SuccessPluginMethodResult.apply(this, _: AnyRef)).recover {
+    case i: InvocationTargetException =>
+      FailurePluginMethodResult(this, i.getTargetException)
+    case throwable: Throwable =>
+      FailurePluginMethodResult(this, throwable)
+  }.get
+}
+
+object PluginMethod {
+  /** Default priority for a plugin method if not marked explicitly. */
+  val DefaultPriority: Long = 0
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/PluginMethodResult.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/PluginMethodResult.scala b/plugins/src/main/scala/org/apache/toree/plugins/PluginMethodResult.scala
new file mode 100644
index 0000000..c82d7f5
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/PluginMethodResult.scala
@@ -0,0 +1,75 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import scala.util.{Failure, Success, Try}
+
+/**
+ * Represents a result from executing a plugin method.
+ */
+sealed trait PluginMethodResult {
+  /** Represents the name of the plugin from which the result originated. */
+  lazy val pluginName: String = pluginMethod.plugin.name
+
+  /** Represents the name of the method from which the result originated. */
+  lazy val methodName: String = pluginMethod.method.getName
+
+  /** Represents the priority of the plugin from which the result originated. */
+  lazy val pluginPriority: Long = pluginMethod.plugin.priority
+
+  /** Represents the priority of the method from which the result originated. */
+  lazy val methodPriority: Long = pluginMethod.priority
+
+  /** Indicates whether or not this result is a success. */
+  lazy val isSuccess: Boolean = toTry.isSuccess
+
+  /** Indicates whether or not this result is a failure. */
+  lazy val isFailure: Boolean = toTry.isFailure
+
+  /** Represents the plugin method instance from which the result originated. */
+  val pluginMethod: PluginMethod
+
+  /** Converts result to a try. */
+  def toTry: Try[AnyRef]
+}
+
+/**
+ * A successful result from executing a plugin method.
+ *
+ * @param pluginMethod The method that was executed
+ * @param result The result from the execution
+ */
+case class SuccessPluginMethodResult(
+  pluginMethod: PluginMethod,
+  result: AnyRef
+) extends PluginMethodResult {
+  val toTry: Try[AnyRef] = Success(result)
+}
+
+/**
+ * A failed result from executing a plugin method.
+ *
+ * @param pluginMethod The method that was executed
+ * @param throwable The error that was thrown
+ */
+case class FailurePluginMethodResult(
+  pluginMethod: PluginMethod,
+  throwable: Throwable
+) extends PluginMethodResult {
+  val toTry: Try[AnyRef] = Failure(throwable)
+}
+

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/PluginSearcher.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/PluginSearcher.scala b/plugins/src/main/scala/org/apache/toree/plugins/PluginSearcher.scala
new file mode 100644
index 0000000..e6c9141
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/PluginSearcher.scala
@@ -0,0 +1,106 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.io.File
+import org.clapper.classutil.{ClassInfo, ClassFinder}
+import org.slf4j.LoggerFactory
+
+import scala.annotation.tailrec
+import scala.util.Try
+
+/**
+ * Represents the search utility for locating plugin classes.
+ */
+class PluginSearcher {
+  /** Represents logger used by plugin searcher. */
+  private val logger = LoggerFactory.getLogger(this.getClass)
+
+  /** Contains all internal plugins for the system. */
+  lazy val internal: Seq[ClassInfo] = findPluginClasses(newClassFinder()).toSeq
+
+  /**
+   * Searches in the provided paths (jars/zips/directories) for plugin classes.
+   *
+   * @param paths The paths to search through
+   * @return An iterator over plugin class information
+   */
+  def search(paths: File*): Iterator[ClassInfo] = {
+    findPluginClasses(newClassFinder(paths))
+  }
+
+  /**
+   * Creates a new class finder using the JVM classpath.
+   *
+   * @return The new class finder
+   */
+  protected def newClassFinder(): ClassFinder = ClassFinder()
+
+  /**
+   * Creates a new class finder for the given paths.
+   *
+   * @param paths The paths within which to search for classes
+   *
+   * @return The new class finder
+   */
+  protected def newClassFinder(paths: Seq[File]): ClassFinder = ClassFinder(paths)
+
+  /**
+   * Searches for classes implementing in the plugin interface, directly or
+   * indirectly.
+   *
+   * @param classFinder The class finder from which to retrieve class information
+   * @return An iterator over plugin class information
+   */
+  private def findPluginClasses(classFinder: ClassFinder): Iterator[ClassInfo] = {
+    val tryStream = Try(classFinder.getClasses())
+    tryStream.failed.foreach(logger.error(
+      s"Failed to find plugins from classpath: ${classFinder.classpath.mkString(",")}",
+      _: Throwable
+    ))
+    val stream = tryStream.getOrElse(Stream.empty)
+    val classMap = ClassFinder.classInfoMap(stream.toIterator)
+    concreteSubclasses(classOf[Plugin].getName, classMap)
+  }
+
+  /** Patched search that also traverses interfaces. */
+  private def concreteSubclasses(
+    ancestor: String,
+    classes: Map[String, ClassInfo]
+  ): Iterator[ClassInfo] = {
+    @tailrec def classMatches(
+      ancestorClassInfo: ClassInfo,
+      classesToCheck: Seq[ClassInfo]
+    ): Boolean = {
+      if (classesToCheck.isEmpty) false
+      else if (classesToCheck.exists(_.name == ancestorClassInfo.name)) true
+      else if (classesToCheck.exists(_.superClassName == ancestorClassInfo.name)) true
+      else if (classesToCheck.exists(_ implements ancestorClassInfo.name)) true
+      else {
+        val superClasses = classesToCheck.map(_.superClassName).flatMap(classes.get)
+        val interfaces = classesToCheck.flatMap(_.interfaces).flatMap(classes.get)
+        classMatches(ancestorClassInfo, superClasses ++ interfaces)
+      }
+    }
+
+    classes.get(ancestor).map(ci => {
+      classes.values.toIterator
+        .filter(_.isConcrete)
+        .filter(c => classMatches(ci, Seq(c)))
+    }).getOrElse(Iterator.empty)
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/UnknownPluginTypeException.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/UnknownPluginTypeException.scala b/plugins/src/main/scala/org/apache/toree/plugins/UnknownPluginTypeException.scala
new file mode 100644
index 0000000..a205341
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/UnknownPluginTypeException.scala
@@ -0,0 +1,26 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+/**
+ * Represents an error that occurs when trying to load a plugin of an unknown
+ * type.
+ *
+ * @param name The full class name of the plugin
+ */
+class UnknownPluginTypeException(name: String)
+  extends Throwable(s"Unknown plugin type: $name")

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/dependencies/Dependency.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/dependencies/Dependency.scala b/plugins/src/main/scala/org/apache/toree/plugins/dependencies/Dependency.scala
new file mode 100644
index 0000000..fd016ec
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/dependencies/Dependency.scala
@@ -0,0 +1,80 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins.dependencies
+
+import scala.reflect.runtime.universe.{Type, TypeTag}
+
+/**
+ * Represents a dependency.
+ *
+ * @param name The name of the dependency
+ * @param `type` The type of the dependency
+ * @param value The value of the dependency
+ */
+case class Dependency[T <: AnyRef](
+  name: String,
+  `type`: Type,
+  value: T
+) {
+  require(name != null, "Name cannot be null!")
+  require(name.nonEmpty, "Name must not be empty!")
+  require(`type` != null, "Type cannot be null!")
+  require(value != null, "Value cannot be null!")
+
+  /**
+   * Returns the Java class representation of this dependency's type.
+   *
+   * @param classLoader The class loader to use when acquiring the Java class
+   * @return The Java class instance
+   */
+  def typeClass(classLoader: ClassLoader): Class[_] = {
+    import scala.reflect.runtime.universe._
+    val m = runtimeMirror(classLoader)
+    m.runtimeClass(`type`.typeSymbol.asClass)
+  }
+
+  /** Represents the class for the dependency's value. */
+  val valueClass = value.getClass
+}
+
+object Dependency {
+  /**
+   * Creates a dependency using the provided value, generating a unique name.
+   * @param value The value of the dependency
+   * @return The new dependency instance
+   */
+  def fromValue[T <: AnyRef : TypeTag](value: T) = fromValueWithName(
+    java.util.UUID.randomUUID().toString,
+    value
+  )
+
+  /**
+   * Creates a dependency using the provided name and value.
+   * @param name The name of the dependency
+   * @param value The value of the dependency
+   * @param typeTag The type information for the dependency's value
+   * @return The new dependency instance
+   */
+  def fromValueWithName[T <: AnyRef : TypeTag](
+    name: String,
+    value: T
+  )(implicit typeTag: TypeTag[T]) = Dependency(
+    name = name,
+    `type` = typeTag.tpe,
+    value = value
+  )
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyException.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyException.scala b/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyException.scala
new file mode 100644
index 0000000..d00edef
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyException.scala
@@ -0,0 +1,56 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins.dependencies
+
+/** Represents a generic dependency exception. */
+sealed class DependencyException(message: String) extends Throwable(message)
+
+/**
+ * Represents a dependency exception where the dependency with the desired
+ * name was not found.
+ *
+ * @param name The name of the missing dependency
+ */
+class DepNameNotFoundException(name: String) extends DependencyException(
+  s"Dependency with name '$name' not found!"
+)
+
+/**
+ * Represents a dependency exception where the dependency with the desired
+ * class type was not found.
+ *
+ * @param klass The class from which the dependency extends
+ */
+class DepClassNotFoundException(klass: Class[_]) extends DependencyException(
+  s"Dependency extending class '${klass.getName}' not found!"
+)
+
+/**
+ * Represents a dependency exception where the dependency with the desired
+ * name was found but the class did not match.
+ *
+ * @param name The name of the dependency
+ * @param expected The desired dependency class
+ * @param actual The class from which the found dependency extends
+ */
+class DepUnexpectedClassException(
+  name: String,
+  expected: Class[_],
+  actual: Class[_]
+) extends DependencyException(
+  s"Dependency found called '$name', but expected '$expected' and had class '$actual'!"
+)
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyManager.scala
----------------------------------------------------------------------
diff --git a/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyManager.scala b/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyManager.scala
new file mode 100644
index 0000000..ab6adfb
--- /dev/null
+++ b/plugins/src/main/scala/org/apache/toree/plugins/dependencies/DependencyManager.scala
@@ -0,0 +1,197 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins.dependencies
+
+import java.util.concurrent.ConcurrentHashMap
+import scala.collection.JavaConverters._
+import scala.reflect.runtime.universe.{Type, TypeTag}
+import scala.util.Try
+
+/**
+ * Contains helpers and contants associated with the dependency manager.
+ */
+object DependencyManager {
+  /** Represents an empty dependency manager. */
+  val Empty = new DependencyManager {
+    // Prevent adding dependencies
+    override def add[T <: AnyRef](dependency: Dependency[T]): Unit = {}
+  }
+
+  /**
+   * Creates a new dependency manager using the provided dependencies.
+   *
+   * @param dependencies The collection of dependencies for the manager
+   */
+  def from(dependencies: Dependency[_ <: AnyRef]*): DependencyManager = {
+    val dm = new DependencyManager
+    dependencies.foreach(d => dm.add(d))
+    dm
+  }
+}
+
+/**
+ * Represents manager of dependencies by name and type.
+ */
+class DependencyManager {
+  private val dependencies: collection.mutable.Map[String, Dependency[_ <: AnyRef]] =
+    new ConcurrentHashMap[String, Dependency[_ <: AnyRef]]().asScala
+
+  /**
+   * Merges this dependency manager with another, overwriting any conflicting
+   * dependencies (by name) with the other dependency manager.
+   *
+   * @param dependencyManager The other dependency manager to merge
+   * @return The new dependency manager
+   */
+  def merge(dependencyManager: DependencyManager): DependencyManager = {
+    val dm = DependencyManager.from(dependencyManager.toSeq: _*)
+
+    // Ignore any conflicts by not overwriting
+    toSeq.foreach(d => Try(dm.add(d)))
+
+    dm
+  }
+
+  /**
+   * Returns a map of dependency names to values.
+   *
+   * @return The map of dependency names and values
+   */
+  def toMap: Map[String, Any] =
+    dependencies.values.map(d => d.name -> d.value).toMap
+
+  /**
+   * Returns a sequence of dependencies contained by this manager.
+   *
+   * @return The sequence of dependency objects
+   */
+  def toSeq: Seq[Dependency[_ <: AnyRef]] = dependencies.values.toSeq
+
+  /**
+   * Adds a new dependency to the manager.
+   *
+   * @param value The value of the dependency
+   * @tparam T The dependency's type
+   */
+  def add[T <: AnyRef : TypeTag](value: T): Unit =
+    add(java.util.UUID.randomUUID().toString, value)
+
+  /**
+   * Adds a new dependency to the manager.
+   *
+   * @param name The name of the dependency
+   * @param value The value of the dependency
+   * @param typeTag The type information collected about the dependency
+   * @tparam T The dependency's type
+   */
+  def add[T <: AnyRef](name: String, value: T)(implicit typeTag: TypeTag[T]): Unit =
+    add(Dependency(name, typeTag.tpe, value))
+
+  /**
+   * Adds a new dependency to the manager.
+   *
+   * @param dependency The dependency construct containing all relevant info
+   * @tparam T The dependency's type
+   */
+  def add[T <: AnyRef](dependency: Dependency[T]): Unit = {
+    require(!dependencies.contains(dependency.name))
+    dependencies.put(dependency.name, dependency)
+  }
+
+  /**
+   * Finds a dependency with the matching name in this manager.
+   *
+   * @param name The name of the dependency
+   * @return Some dependency if found, otherwise None
+   */
+  def find(name: String): Option[Dependency[_]] = dependencies.get(name)
+
+  /**
+   * Finds all dependencies whose type matches or is a subclass of the
+   * specified type.
+   *
+   * @param `type` The type to match against each dependency's type
+   * @return The collection of matching dependencies
+   */
+  def findByType(`type`: Type): Seq[Dependency[_]] =
+    dependencies.values.filter(_.`type` <:< `type`).toSeq
+
+  /**
+   * Finds all dependencies whose type class representation matches or is a
+   * subclass of the specified class.
+   *
+   * @param klass The class to match against the dependency's
+   *              type class representation
+   * @return The collection of matching dependencies
+   */
+  def findByTypeClass(klass: Class[_]): Seq[Dependency[_]] =
+    dependencies.values.filter(d =>
+      klass.isAssignableFrom(d.typeClass(klass.getClassLoader))
+    ).toSeq
+
+  /**
+   * Finds all dependencies whose value class representation matches or is a
+   * subclass of the specified class.
+   *
+   * @param klass The class to match against the dependency's
+   *              value class representation
+   * @return The collection of matching dependencies
+   */
+  def findByValueClass(klass: Class[_]): Seq[Dependency[_]] =
+    dependencies.values.filter(d => klass.isAssignableFrom(d.valueClass)).toSeq
+
+  /**
+   * Removes the dependency with the specified name.
+   *
+   * @param name The name of the dependency
+   * @return Some dependency if removed, otherwise None
+   */
+  def remove(name: String): Option[Dependency[_]] =
+    dependencies.remove(name)
+
+  /**
+   * Removes all dependencies whose type matches or is a subclass of the
+   * specified type.
+   *
+   * @param `type` The type to match against each dependency's type
+   * @return The collection of matching dependencies
+   */
+  def removeByType(`type`: Type): Seq[Dependency[_]] =
+    findByType(`type`).map(_.name).flatMap(remove)
+
+  /**
+   * Removes all dependencies whose type class representation matches or is a
+   * subclass of the specified class.
+   *
+   * @param klass The class to match against the dependency's
+   *              type class representation
+   * @return The collection of matching dependencies
+   */
+  def removeByTypeClass(klass: Class[_]): Seq[Dependency[_]] =
+    findByTypeClass(klass).map(_.name).flatMap(remove)
+
+  /**
+   * Removes all dependencies whose value class representation matches or is a
+   * subclass of the specified class.
+   *
+   * @param klass The class to match against the dependency's
+   *              value class representation
+   * @return The collection of matching dependencies
+   */
+  def removeByValueClass(klass: Class[_]): Seq[Dependency[_]] =
+    findByValueClass(klass).map(_.name).flatMap(remove)
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/integration/PluginManagerSpecForIntegration.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/integration/PluginManagerSpecForIntegration.scala b/plugins/src/test/scala/integration/PluginManagerSpecForIntegration.scala
new file mode 100644
index 0000000..b2e9d00
--- /dev/null
+++ b/plugins/src/test/scala/integration/PluginManagerSpecForIntegration.scala
@@ -0,0 +1,76 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package integration
+
+import org.apache.toree.plugins.{PluginManager, Plugin}
+import org.apache.toree.plugins.annotations.Init
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+
+class PluginManagerSpecForIntegration extends FunSpec with Matchers
+  with OneInstancePerTest
+{
+  private val pluginManager = new PluginManager
+
+  describe("PluginManager") {
+    it("should be able to initialize plugins with dependencies provided by other plugins") {
+      val cpa = pluginManager.loadPlugin("", classOf[ConsumePluginA]).get
+      val rpa = pluginManager.loadPlugin("", classOf[RegisterPluginA]).get
+
+      val results = pluginManager.initializePlugins(Seq(cpa, rpa))
+
+      results.forall(_.isSuccess) should be (true)
+    }
+
+    it("should fail when plugins have circular dependencies") {
+      val cp = pluginManager.loadPlugin("", classOf[CircularPlugin]).get
+
+      val results = pluginManager.initializePlugins(Seq(cp))
+
+      results.forall(_.isFailure) should be (true)
+    }
+
+    it("should be able to handle non-circular dependencies within the same plugin") {
+      val ncp = pluginManager.loadPlugin("", classOf[NonCircularPlugin]).get
+
+      val results = pluginManager.initializePlugins(Seq(ncp))
+
+      results.forall(_.isSuccess) should be (true)
+    }
+  }
+}
+
+private class DepA
+private class DepB
+
+private class CircularPlugin extends Plugin {
+  @Init def initMethodA(depA: DepA) = register(new DepB)
+  @Init def initMethodB(depB: DepB) = register(new DepA)
+}
+
+private class NonCircularPlugin extends Plugin {
+  @Init def initMethodB(depB: DepB) = {}
+  @Init def initMethodA(depA: DepA) = register(new DepB)
+  @Init def initMethod() = register(new DepA)
+}
+
+private class RegisterPluginA extends Plugin {
+  @Init def initMethod() = register(new DepA)
+}
+
+private class ConsumePluginA extends Plugin {
+  @Init def initMethod(depA: DepA) = {}
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/ImplicitsSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/ImplicitsSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/ImplicitsSpec.scala
new file mode 100644
index 0000000..247bfa7
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/ImplicitsSpec.scala
@@ -0,0 +1,54 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+
+package org.apache.toree.plugins
+
+import org.apache.toree.plugins.dependencies.Dependency
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+
+class ImplicitsSpec extends FunSpec with Matchers with OneInstancePerTest {
+  describe("Implicits") {
+    describe("#$dep") {
+      it("should convert values to dependencies with generated names") {
+        import scala.reflect.runtime.universe._
+        import org.apache.toree.plugins.Implicits._
+
+        val value = new Object
+
+        val d: Dependency[_] = value
+
+        d.name should not be (empty)
+        d.`type` should be (typeOf[Object])
+        d.value should be (value)
+      }
+
+      it("should convert tuples of (string, value) to dependencies with the specified names") {
+        import scala.reflect.runtime.universe._
+        import org.apache.toree.plugins.Implicits._
+
+        val name = "some name"
+        val value = new Object
+
+        val d: Dependency[_] = name -> value
+
+        d.name should be (name)
+        d.`type` should be (typeOf[Object])
+        d.value should be (value)
+      }
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-toree/blob/4c0dccfb/plugins/src/test/scala/org/apache/toree/plugins/PluginClassLoaderSpec.scala
----------------------------------------------------------------------
diff --git a/plugins/src/test/scala/org/apache/toree/plugins/PluginClassLoaderSpec.scala b/plugins/src/test/scala/org/apache/toree/plugins/PluginClassLoaderSpec.scala
new file mode 100644
index 0000000..da61f5e
--- /dev/null
+++ b/plugins/src/test/scala/org/apache/toree/plugins/PluginClassLoaderSpec.scala
@@ -0,0 +1,55 @@
+/*
+ *  Licensed to the Apache Software Foundation (ASF) under one or more
+ *  contributor license agreements.  See the NOTICE file distributed with
+ *  this work for additional information regarding copyright ownership.
+ *  The ASF licenses this file to You under the Apache License, Version 2.0
+ *  (the "License"); you may not use this file except in compliance with
+ *  the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License
+ */
+package org.apache.toree.plugins
+
+import java.io.File
+
+import org.scalatest.{OneInstancePerTest, Matchers, FunSpec}
+
+class PluginClassLoaderSpec extends FunSpec with Matchers
+  with OneInstancePerTest
+{
+  describe("PluginClassLoader") {
+    describe("#addURL") {
+      it("should add the url if not already in the loader") {
+        val expected = Seq(new File("/some/file").toURI.toURL)
+
+        val pluginClassLoader = new PluginClassLoader(Nil, null)
+
+        // Will add for first time
+        expected.foreach(pluginClassLoader.addURL)
+
+        val actual = pluginClassLoader.getURLs
+
+        actual should contain theSameElementsAs (expected)
+      }
+
+      it("should not add the url if already in the loader") {
+        val expected = Seq(new File("/some/file").toURI.toURL)
+
+        val pluginClassLoader = new PluginClassLoader(expected, null)
+
+        // Will not add again
+        expected.foreach(pluginClassLoader.addURL)
+
+        val actual = pluginClassLoader.getURLs
+
+        actual should contain theSameElementsAs (expected)
+      }
+    }
+  }
+}