diff --git a/pom.xml b/pom.xml index 2e75d2ad47a..e7343d76d0c 100755 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,7 @@ zeppelin-interpreter zeppelin-zengine + zeppelin-display spark-dependencies spark markdown diff --git a/spark/pom.xml b/spark/pom.xml index 773bbc9be18..a1c28af7832 100644 --- a/spark/pom.xml +++ b/spark/pom.xml @@ -52,6 +52,12 @@ slf4j-log4j12 + + ${project.groupId} + zeppelin-display + ${project.version} + + ${project.groupId} zeppelin-interpreter diff --git a/zeppelin-display/pom.xml b/zeppelin-display/pom.xml new file mode 100644 index 00000000000..1a8328ec277 --- /dev/null +++ b/zeppelin-display/pom.xml @@ -0,0 +1,187 @@ + + + + + 4.0.0 + + + zeppelin + org.apache.zeppelin + 0.6.0-incubating-SNAPSHOT + .. + + + org.apache.zeppelin + zeppelin-display + jar + 0.6.0-incubating-SNAPSHOT + Zeppelin: Display system apis + http://incubator.zeppelin.apache.org + + + 2.10.4 + 2.10 + + + + + + org.scala-lang + scala-library + ${scala.version} + + + + org.scala-lang + scala-compiler + ${scala.version} + + + + org.scala-lang + scalap + ${scala.version} + + + + + + + ${project.groupId} + zeppelin-interpreter + ${project.version} + + + + org.slf4j + slf4j-api + + + + org.slf4j + slf4j-log4j12 + + + + junit + junit + test + + + + org.scala-lang + scala-library + + + + org.scalatest + scalatest_2.10 + 2.1.1 + test + + + + + + + org.apache.rat + apache-rat-plugin + + + **/.idea/ + **/*.iml + .git/ + .gitignore + **/.settings/* + **/.classpath + **/.project + **/target/** + **/README.md + + + + + + maven-failsafe-plugin + 2.16 + + + + integration-test + verify + + + + + -Xmx2048m + + + + + org.apache.maven.plugins + maven-deploy-plugin + 2.7 + + true + + + + + org.scala-tools + maven-scala-plugin + 2.15.2 + + + compile + + compile + + compile + + + test-compile + + testCompile + + test-compile + + + process-resources + + compile + + + + + + + org.scalatest + scalatest-maven-plugin + 1.0 + + + test + + test + + + + + + + diff --git a/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/AbstractAngularElem.scala b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/AbstractAngularElem.scala new file mode 100644 index 00000000000..80e36996bd1 --- /dev/null +++ b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/AbstractAngularElem.scala @@ -0,0 +1,192 @@ +/* + * 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.zeppelin.display.angular + +import java.io.PrintStream + +import org.apache.zeppelin.display.{AngularObjectWatcher, AngularObject} +import org.apache.zeppelin.interpreter.{InterpreterResult, InterpreterContext} + +import scala.xml._ + +/** + * Element that binded to Angular object + */ +abstract class AbstractAngularElem(val interpreterContext: InterpreterContext, + val modelName: String, + val angularObjects: Map[String, AngularObject[Any]], + prefix: String, + label: String, + attributes1: MetaData, + scope: NamespaceBinding, + minimizeEmpty: Boolean, + child: Node*) + extends Elem(prefix, label, attributes1, scope, minimizeEmpty, child:_*) { + + val uniqueId = java.util.UUID.randomUUID.toString.replaceAll("-", "_") + + /** + * On click element + * @param callback + * @return + */ + def onClick(callback: () => Unit): AbstractAngularElem = { + onEvent("ng-click", callback) + } + + /** + * On + * @param callback + * @return + */ + def onChange(callback: () => Unit): AbstractAngularElem = { + onEvent("ng-change", callback) + } + + /** + * Bind angularObject to ng-model directive + * @param name name of angularObject + * @param value initialValue + * @return + */ + def model(name: String, value: Any): AbstractAngularElem = { + val registry = interpreterContext.getAngularObjectRegistry + + // create AngularFunction in current paragraph + val elem = this % Attribute(None, "ng-model", + Text(s"${name}"), + Null) + + val angularObject = addAngularObject(name, value) + .asInstanceOf[AngularObject[Any]] + + newElem( + interpreterContext, + name, + angularObjects + (name -> angularObject), + elem) + } + + + def model(name: String): AbstractAngularElem = { + val registry = interpreterContext.getAngularObjectRegistry + + // create AngularFunction in current paragraph + val elem = this % Attribute(None, "ng-model", + Text(s"${name}"), + Null) + + newElem( + interpreterContext, + name, + angularObjects, + elem) + } + + /** + * Retrieve value of model + * @return + */ + def model(): Any = { + if (angularObjects.contains(modelName)) { + angularObjects(modelName).get() + } else { + None + } + } + + /** + * + * @param eventName angular directive like ng-click, ng-change, etc. + * @return + */ + def onEvent(eventName: String, callback: () => Unit): AbstractAngularElem = { + val registry = interpreterContext.getAngularObjectRegistry + + // create AngularFunction in current paragraph + val functionName = eventName.replaceAll("-", "_") + "_" + uniqueId + val elem = this % Attribute(None, eventName, + Text(s"${functionName}=$$event.timeStamp"), + Null) + + val angularObject = addAngularObject(functionName, "") + + angularObject.addWatcher(new AngularObjectWatcher(interpreterContext) { + override def watch(oldObject: scala.Any, newObject: scala.Any, context: InterpreterContext) + :Unit = { + InterpreterContext.set(interpreterContext) + callback() + } + }) + + newElem( + interpreterContext, + modelName, + angularObjects + (eventName -> angularObject), + elem) + } + + protected def addAngularObject(name: String, value: Any): AngularObject[Any] + + protected def newElem(interpreterContext: InterpreterContext, + name: String, + angularObjects: Map[String, AngularObject[Any]], + elem: scala.xml.Elem): AbstractAngularElem + + /** + * disassociate this element and it's child from front-end + * by removing angularobject + */ + def disassociate() = { + remove(this) + } + + /** + * Remove all angularObject recursively + * @param node + */ + private def remove(node: Node): Unit = { + if (node.isInstanceOf[AbstractAngularElem]) { + node.asInstanceOf[AbstractAngularElem].angularObjects.values.foreach{ ao => + interpreterContext.getAngularObjectRegistry.remove(ao.getName, ao.getNoteId, ao + .getParagraphId) + } + } + + node.child.foreach(remove _) + } + + /** + * Print into provided print stream + * @return + */ + def display(out: java.io.PrintStream): Unit = { + out.print(this.toString) + out.flush() + } + + /** + * Print into InterpreterOutput + */ + def display(): Unit = { + val out = interpreterContext.out + out.setType(InterpreterResult.Type.ANGULAR) + out.write(this.toString()) + out.flush() + } +} + diff --git a/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/AbstractAngularModel.scala b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/AbstractAngularModel.scala new file mode 100644 index 00000000000..ff504380412 --- /dev/null +++ b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/AbstractAngularModel.scala @@ -0,0 +1,95 @@ +/* + * 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.zeppelin.display.angular + +import org.apache.zeppelin.display.AngularObject +import org.apache.zeppelin.interpreter.InterpreterContext + +/** + * Represents ng-model with angular object + */ +abstract class AbstractAngularModel(name: String) { + val context = InterpreterContext.get + val registry = context.getAngularObjectRegistry + + + /** + * Create AngularModel with initial Value + * @param name name of model + * @param newValue value + */ + def this(name: String, newValue: Any) = { + this(name) + value(newValue) + } + + protected def getAngularObject(): AngularObject[Any] + protected def addAngularObject(value: Any): AngularObject[Any] + + /** + * Get value of the model + * @return + */ + def apply(): Any = { + value() + } + + /** + * Get value of the model + * @return + */ + def value(): Any = { + val angularObject = getAngularObject() + if (angularObject == null) { + None + } else { + angularObject.get + } + } + + + def apply(newValue: Any): Unit = { + value(newValue) + } + + + /** + * Set value of the model + * @param newValue + */ + def value(newValue: Any): Unit = { + var angularObject = getAngularObject() + if (angularObject == null) { + // create new object + angularObject = addAngularObject(newValue) + } else { + angularObject.set(newValue) + } + angularObject.get() + } + + def remove(): Any = { + val angularObject = getAngularObject() + + if (angularObject == null) { + None + } else { + registry.remove(name, angularObject.getNoteId(), angularObject.getParagraphId()) + angularObject.get + } + } +} diff --git a/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/notebookscope/AngularElem.scala b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/notebookscope/AngularElem.scala new file mode 100644 index 00000000000..9ba88ffd916 --- /dev/null +++ b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/notebookscope/AngularElem.scala @@ -0,0 +1,84 @@ +/* + * 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.zeppelin.display.angular.notebookscope + +import org.apache.zeppelin.display.angular.AbstractAngularElem +import org.apache.zeppelin.display.{angular, AngularObject} +import org.apache.zeppelin.interpreter.InterpreterContext + +import scala.collection.JavaConversions +import scala.xml._ + +/** + * AngularElement in notebook scope + */ +class AngularElem(override val interpreterContext: InterpreterContext, + override val modelName: String, + override val angularObjects: Map[String, AngularObject[Any]], + prefix: String, + label: String, + attributes1: MetaData, + scope: NamespaceBinding, + minimizeEmpty: Boolean, + child: Node*) + extends AbstractAngularElem( + interpreterContext, modelName, angularObjects, prefix, label, attributes1, scope, + minimizeEmpty, child: _*) { + + override protected def addAngularObject(name: String, value: Any): AngularObject[Any] = { + val registry = interpreterContext.getAngularObjectRegistry + registry.add(name, value, interpreterContext.getNoteId, null).asInstanceOf[AngularObject[Any]] + + } + + override protected def newElem(interpreterContext: InterpreterContext, + name: String, + angularObjects: Map[String, AngularObject[Any]], + elem: scala.xml.Elem): angular.AbstractAngularElem = { + new AngularElem( + interpreterContext, + name, + angularObjects, + elem.prefix, + elem.label, + elem.attributes, + elem.scope, + elem.minimizeEmpty, + elem.child:_*) + } +} + +object AngularElem { + implicit def Elem2AngularDisplayElem(elem: Elem): AbstractAngularElem = { + new AngularElem(InterpreterContext.get(), null, + Map[String, AngularObject[Any]](), + elem.prefix, elem.label, elem.attributes, elem.scope, elem.minimizeEmpty, elem.child:_*); + } + + /** + * Disassociate (remove) all angular object in this notebook + */ + def disassociate() = { + val ic = InterpreterContext.get + val registry = ic.getAngularObjectRegistry + + JavaConversions.asScalaBuffer(registry.getAll(ic.getNoteId, null)).foreach(ao => + registry.remove(ao.getName, ao.getNoteId, null) + ) + } +} \ No newline at end of file diff --git a/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/notebookscope/AngularModel.scala b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/notebookscope/AngularModel.scala new file mode 100644 index 00000000000..1ef89831204 --- /dev/null +++ b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/notebookscope/AngularModel.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 org.apache.zeppelin.display.angular.notebookscope + +import org.apache.zeppelin.display.AngularObject +import org.apache.zeppelin.display.angular.AbstractAngularModel +import org.apache.zeppelin.interpreter.InterpreterContext + +/** + * Represents ng-model in notebook scope + */ +class AngularModel(name: String) + extends org.apache.zeppelin.display.angular.AbstractAngularModel(name) { + + def this(name: String, newValue: Any) = { + this(name) + value(newValue) + } + + override protected def getAngularObject(): AngularObject[Any] = { + registry.get(name, context.getNoteId, null).asInstanceOf[AngularObject[Any]] + } + + override protected def addAngularObject(value: Any): AngularObject[Any] = { + registry.add(name, value, context.getNoteId, null).asInstanceOf[AngularObject[Any]] + } +} + + +object AngularModel { + def apply(name: String): AbstractAngularModel = { + new AngularModel(name) + } + + def apply(name: String, newValue: Any): AbstractAngularModel = { + new AngularModel(name, newValue) + } +} \ No newline at end of file diff --git a/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularElem.scala b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularElem.scala new file mode 100644 index 00000000000..50bd0ed4094 --- /dev/null +++ b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularElem.scala @@ -0,0 +1,86 @@ +/* + * 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.zeppelin.display.angular.paragraphscope + + +import org.apache.zeppelin.display.angular.AbstractAngularElem +import org.apache.zeppelin.display.{angular, AngularObject} +import org.apache.zeppelin.interpreter.InterpreterContext + +import scala.collection.JavaConversions +import scala.xml._ + +/** + * AngularElement in paragraph scope + */ +class AngularElem(override val interpreterContext: InterpreterContext, + override val modelName: String, + override val angularObjects: Map[String, AngularObject[Any]], + prefix: String, + label: String, + attributes1: MetaData, + scope: NamespaceBinding, + minimizeEmpty: Boolean, + child: Node*) + extends AbstractAngularElem( + interpreterContext, modelName, angularObjects, prefix, label, attributes1, scope, + minimizeEmpty, child: _*) { + + override protected def addAngularObject(name: String, value: Any): AngularObject[Any] = { + val registry = interpreterContext.getAngularObjectRegistry + registry.add(name, value, interpreterContext.getNoteId, interpreterContext.getParagraphId) + .asInstanceOf[AngularObject[Any]] + + } + + override protected def newElem(interpreterContext: InterpreterContext, + name: String, + angularObjects: Map[String, AngularObject[Any]], + elem: scala.xml.Elem): angular.AbstractAngularElem = { + new AngularElem( + interpreterContext, + name, + angularObjects, + elem.prefix, + elem.label, + elem.attributes, + elem.scope, + elem.minimizeEmpty, + elem.child:_*) + } +} + +object AngularElem { + implicit def Elem2AngularDisplayElem(elem: Elem): AbstractAngularElem = { + new AngularElem(InterpreterContext.get(), null, + Map[String, AngularObject[Any]](), + elem.prefix, elem.label, elem.attributes, elem.scope, elem.minimizeEmpty, elem.child:_*); + } + + /** + * Disassociate (remove) all angular object in this notebook + */ + def disassociate() = { + val ic = InterpreterContext.get + val registry = ic.getAngularObjectRegistry + + JavaConversions.asScalaBuffer(registry.getAll(ic.getNoteId, ic.getParagraphId)).foreach(ao => + registry.remove(ao.getName, ao.getNoteId, ao.getParagraphId) + ) + } +} \ No newline at end of file diff --git a/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularModel.scala b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularModel.scala new file mode 100644 index 00000000000..ed28687290b --- /dev/null +++ b/zeppelin-display/src/main/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularModel.scala @@ -0,0 +1,53 @@ +/* + * 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.zeppelin.display.angular.paragraphscope + +import org.apache.zeppelin.display.AngularObject +import org.apache.zeppelin.display.angular.AbstractAngularModel + +/** + * Represents ng-model in paragraph scope + */ +class AngularModel(name: String) + extends org.apache.zeppelin.display.angular.AbstractAngularModel(name) { + + def this(name: String, newValue: Any) = { + this(name) + value(newValue) + } + + override protected def getAngularObject(): AngularObject[Any] = { + registry.get(name, + context.getNoteId, context.getParagraphId).asInstanceOf[AngularObject[Any]] + } + + override protected def addAngularObject(value: Any): AngularObject[Any] = { + registry.add(name, value, + context.getNoteId, context.getParagraphId).asInstanceOf[AngularObject[Any]] + } +} + + +object AngularModel { + def apply(name: String): AbstractAngularModel = { + new AngularModel(name) + } + + def apply(name: String, newValue: Any): AbstractAngularModel = { + new AngularModel(name, newValue) + } +} \ No newline at end of file diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala new file mode 100644 index 00000000000..2c82c8e4e66 --- /dev/null +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularElemTest.scala @@ -0,0 +1,154 @@ +/* + * 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.zeppelin.display.angular + +import java.io.{ByteArrayOutputStream, PrintStream} +import java.util + +import org.apache.zeppelin.display.{AngularObject, AngularObjectRegistry, GUI} +import org.apache.zeppelin.interpreter._ +import org.scalatest.concurrent.Eventually +import org.scalatest.{BeforeAndAfter, BeforeAndAfterEach, FlatSpec, Matchers} + +/** + * Test + */ +trait AbstractAngularElemTest + extends FlatSpec with BeforeAndAfter with BeforeAndAfterEach with Eventually with Matchers { + + override def beforeEach() { + val intpGroup = new InterpreterGroup() + val context = new InterpreterContext("note", "paragraph", "title", "text", + new util.HashMap[String, Object](), new GUI(), new AngularObjectRegistry( + intpGroup.getId(), null), + new util.LinkedList[InterpreterContextRunner](), + new InterpreterOutput(new InterpreterOutputListener() { + override def onAppend(out: InterpreterOutput, line: Array[Byte]): Unit = { + // nothing to do + } + + override def onUpdate(out: InterpreterOutput, output: Array[Byte]): Unit = { + // nothing to do + } + })) + + InterpreterContext.set(context) + super.beforeEach() // To be stackable, must call super.beforeEach + } + + def angularElem(elem: scala.xml.Elem): AbstractAngularElem; + def angularModel(name: String): AbstractAngularModel; + + + "AngularElem" should "provide onclick method" in { + registrySize should be(0) + + var a = 0 + val elem = angularElem(
).onClick(() => { + a = a + 1 + }) + elem.angularObjects.get("ng-click") should not be(null) + registrySize should be(1) + + // click create thread for callback function to run. So it'll may not immediately invoked + // after click. therefore eventually should be + click(elem) + eventually { + a should be(1) + } + + click(elem) + eventually { + a should be(2) + } + + // disassociate + elem.disassociate() + registrySize should be(0) + } + + "AngularElem" should "print angular display directive only once in a paragraph" in { + val out = new ByteArrayOutputStream() + val printOut = new PrintStream(out) + + angularElem(
).display(printOut) + out.toString should be("
") + + out.reset + angularElem(
).display(printOut) + out.toString should be("
") + } + + "AngularElem" should "bind angularObject to ng-model directive " in { + angularElem(
) + .model("name", "value").toString should be("
") + angularElem(
).model("name", "value").model() should be("value") + angularElem(
).model() should be(None) + } + + "AngularElem" should "able to disassociate AngularObjects" in { + val elem1 = angularElem(
).model("name1", "value1") + val elem2 = angularElem(
).model("name2", "value2") + val elem3 = angularElem(
).model("name3", "value3") + + registrySize should be(3) + + elem1.disassociate() + registrySize should be(2) + + elem2.disassociate() + elem3.disassociate() + registrySize should be(0) + } + + "AngularElem" should "allow access to InterpreterContext inside of callback function" in { + angularModel("name").value("value") + + var modelValue = "" + + val elem = angularElem(
).onClick(() => + modelValue = angularModel("name")().toString + ) + + click(elem) + + eventually { modelValue should be("value")} + } + + + def registry = { + InterpreterContext.get().getAngularObjectRegistry + } + + def registrySize = { + registry.getAllWithGlobal("note").size() + } + + def noteId = { + InterpreterContext.get().getNoteId + } + + def click(elem: org.apache.zeppelin.display.angular.AbstractAngularElem) = { + fireEvent("ng-click", elem) + } + + // simulate click + def fireEvent(eventName: String, elem: org.apache.zeppelin.display.angular.AbstractAngularElem) = { + val angularObject: AngularObject[Any] = elem.angularObjects(eventName); + angularObject.set("event"); + } +} diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala new file mode 100644 index 00000000000..942390ac8f8 --- /dev/null +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/AbstractAngularModelTest.scala @@ -0,0 +1,97 @@ +/* + * 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.zeppelin.display.angular + +import org.apache.zeppelin.display.{AngularObjectRegistry, GUI} +import org.apache.zeppelin.interpreter._ +import org.scalatest.concurrent.Eventually +import org.scalatest.{BeforeAndAfter, BeforeAndAfterEach, FlatSpec, Matchers} + +/** + * Abstract Test for AngularModel + */ +trait AbstractAngularModelTest extends FlatSpec +with BeforeAndAfter with BeforeAndAfterEach with Eventually with Matchers { + override def beforeEach() { + val intpGroup = new InterpreterGroup() + val context = new InterpreterContext("note", "id", "title", "text", + new java.util.HashMap[String, Object](), new GUI(), new AngularObjectRegistry( + intpGroup.getId(), null), + new java.util.LinkedList[InterpreterContextRunner](), + new InterpreterOutput(new InterpreterOutputListener() { + override def onAppend(out: InterpreterOutput, line: Array[Byte]): Unit = { + // nothing to do + } + + override def onUpdate(out: InterpreterOutput, output: Array[Byte]): Unit = { + // nothing to do + } + })) + + InterpreterContext.set(context) + super.beforeEach() // To be stackable, must call super.beforeEach + } + + def angularModel(name: String): AbstractAngularModel + def angularModel(name: String, value: Any): AbstractAngularModel + + "AngularModel" should "able to create AngularObject" in { + val registry = InterpreterContext.get().getAngularObjectRegistry + registrySize should be(0) + + angularModel("model1")() should be(None) + registrySize should be(0) + + angularModel("model1", "value1")() should be("value1") + registrySize should be(1) + + angularModel("model1")() should be("value1") + registrySize should be(1) + } + + "AngularModel" should "able to update AngularObject" in { + val registry = InterpreterContext.get().getAngularObjectRegistry + + val model1 = angularModel("model1", "value1") + model1() should be("value1") + registrySize should be(1) + + model1.value("newValue1") + model1() should be("newValue1") + registrySize should be(1) + + angularModel("model1", "value2")() should be("value2") + registrySize should be(1) + } + + "AngularModel" should "able to remove AngularObject" in { + angularModel("model1", "value1") + registrySize should be(1) + + angularModel("model1").remove() + registrySize should be(0) + } + + + def registry() = { + InterpreterContext.get().getAngularObjectRegistry + } + + def registrySize() = { + registry().getAllWithGlobal(InterpreterContext.get().getNoteId).size + } +} diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/notebookscope/AngularElemTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/notebookscope/AngularElemTest.scala new file mode 100644 index 00000000000..a3effb05deb --- /dev/null +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/notebookscope/AngularElemTest.scala @@ -0,0 +1,41 @@ +/* + * 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.zeppelin.display.angular.notebookscope + + +import org.apache.zeppelin.display.angular.{AbstractAngularElem, AbstractAngularModel, AbstractAngularElemTest} + +import scala.xml.Elem + +/** + * Test + */ +class AngularElemTest extends AbstractAngularElemTest { + + override def angularElem(elem: Elem): AbstractAngularElem = { + AngularElem.Elem2AngularDisplayElem(elem) + } + + override def angularModel(name: String): AbstractAngularModel = { + AngularModel(name) + } + + "AngularElem" should "able to be created from implicit conversion" in { + import AngularElem._ +
.model("modelname") + } +} diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/notebookscope/AngularModelTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/notebookscope/AngularModelTest.scala new file mode 100644 index 00000000000..10197939bab --- /dev/null +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/notebookscope/AngularModelTest.scala @@ -0,0 +1,32 @@ +/* + * 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.zeppelin.display.angular.notebookscope + +import org.apache.zeppelin.display.angular.{AbstractAngularModel, AbstractAngularModelTest} + +/** + * Test for AngularModel + */ +class AngularModelTest extends AbstractAngularModelTest { + override def angularModel(name: String): AbstractAngularModel = { + AngularModel(name) + } + + override def angularModel(name: String, value: Any): AbstractAngularModel = { + AngularModel(name, value) + } +} diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularElemTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularElemTest.scala new file mode 100644 index 00000000000..4135dedf2ff --- /dev/null +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularElemTest.scala @@ -0,0 +1,41 @@ +/* + * 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.zeppelin.display.angular.paragraphscope + + +import org.apache.zeppelin.display.angular.{AbstractAngularElem, AbstractAngularModel, AbstractAngularElemTest} + +import scala.xml.Elem + +/** + * Test + */ +class AngularElemTest extends AbstractAngularElemTest { + + override def angularElem(elem: Elem): AbstractAngularElem = { + AngularElem.Elem2AngularDisplayElem(elem) + } + + override def angularModel(name: String): AbstractAngularModel = { + AngularModel(name) + } + + "AngularElem" should "able to be created from implicit conversion" in { + import AngularElem._ +
.model("modelname") + } +} diff --git a/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularModelTest.scala b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularModelTest.scala new file mode 100644 index 00000000000..c6e1eb08863 --- /dev/null +++ b/zeppelin-display/src/test/scala/org/apache/zeppelin/display/angular/paragraphscope/AngularModelTest.scala @@ -0,0 +1,32 @@ +/* + * 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.zeppelin.display.angular.paragraphscope + +import org.apache.zeppelin.display.angular.{AbstractAngularModel, AbstractAngularModelTest} + +/** + * Test for AngularModel + */ +class AngularModelTest extends AbstractAngularModelTest { + override def angularModel(name: String): AbstractAngularModel = { + AngularModel(name) + } + + override def angularModel(name: String, value: Any): AbstractAngularModel = { + AngularModel(name, value) + } +} diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java index 27e2f77cee2..52e7ea3482d 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java @@ -29,6 +29,7 @@ import org.apache.zeppelin.display.AngularObjectRegistry; import org.apache.zeppelin.display.Input; import org.apache.zeppelin.interpreter.*; +import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry; import org.apache.zeppelin.notebook.repo.NotebookRepo; import org.apache.zeppelin.notebook.utility.IdHashes; import org.apache.zeppelin.scheduler.Job; @@ -413,7 +414,13 @@ private void removeAllAngularObjectInParagraph(String paragraphId) { for (InterpreterSetting setting : settings) { InterpreterGroup intpGroup = setting.getInterpreterGroup(); AngularObjectRegistry registry = intpGroup.getAngularObjectRegistry(); - registry.removeAll(id, paragraphId); + + if (registry instanceof RemoteAngularObjectRegistry) { + // remove paragraph scope object + ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, paragraphId); + } else { + registry.removeAll(id, paragraphId); + } } } diff --git a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java index cae4210c45f..7a87c92a995 100644 --- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java +++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java @@ -284,8 +284,18 @@ public void removeNote(String id) { for (InterpreterSetting settings : replFactory.get()) { AngularObjectRegistry registry = settings.getInterpreterGroup().getAngularObjectRegistry(); if (registry instanceof RemoteAngularObjectRegistry) { + // remove paragraph scope object + for (Paragraph p : note.getParagraphs()) { + ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, p.getId()); + } + // remove notebook scope object ((RemoteAngularObjectRegistry) registry).removeAllAndNotifyRemoteProcess(id, null); } else { + // remove paragraph scope object + for (Paragraph p : note.getParagraphs()) { + registry.removeAll(id, p.getId()); + } + // remove notebook scope object registry.removeAll(id, null); } } diff --git a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java index 506b682b293..55cd6ba6714 100644 --- a/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java +++ b/zeppelin-zengine/src/test/java/org/apache/zeppelin/notebook/NotebookTest.java @@ -320,18 +320,59 @@ public void testAngularObjectRemovalOnNotebookRemove() throws InterruptedExcepti .getInterpreterSettings().get(0).getInterpreterGroup() .getAngularObjectRegistry(); - // add local scope object - registry.add("o1", "object1", note.id(), null); + Paragraph p1 = note.addParagraph(); + + // add paragraph scope object + registry.add("o1", "object1", note.id(), p1.getId()); + + // add notebook scope object + registry.add("o2", "object2", note.id(), null); + // add global scope object - registry.add("o2", "object2", null, null); + registry.add("o3", "object3", null, null); // remove notebook notebook.removeNote(note.id()); - // local object should be removed + // notebook scope or paragraph scope object should be removed assertNull(registry.get("o1", note.id(), null)); + assertNull(registry.get("o2", note.id(), p1.getId())); + // global object sould be remained - assertNotNull(registry.get("o2", null, null)); + assertNotNull(registry.get("o3", null, null)); + } + + @Test + public void testAngularObjectRemovalOnParagraphRemove() throws InterruptedException, + IOException { + // create a note and a paragraph + Note note = notebook.createNote(); + note.getNoteReplLoader().setInterpreters(factory.getDefaultInterpreterSettingList()); + + AngularObjectRegistry registry = note.getNoteReplLoader() + .getInterpreterSettings().get(0).getInterpreterGroup() + .getAngularObjectRegistry(); + + Paragraph p1 = note.addParagraph(); + + // add paragraph scope object + registry.add("o1", "object1", note.id(), p1.getId()); + + // add notebook scope object + registry.add("o2", "object2", note.id(), null); + + // add global scope object + registry.add("o3", "object3", null, null); + + // remove notebook + note.removeParagraph(p1.getId()); + + // paragraph scope should be removed + assertNull(registry.get("o1", note.id(), null)); + + // notebook scope and global object sould be remained + assertNotNull(registry.get("o2", note.id(), null)); + assertNotNull(registry.get("o3", null, null)); } @Test