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)
+ }
+ }
+ }
+}