You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by zj...@apache.org on 2019/02/03 02:55:14 UTC

[zeppelin] branch master updated: [ZEPPELIN-3931] Redisplay angularObjectBind when the note is reopened

This is an automated email from the ASF dual-hosted git repository.

zjffdu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 62da77c  [ZEPPELIN-3931] Redisplay angularObjectBind when the note is reopened
62da77c is described below

commit 62da77ca0bb61d5bffecfa9123d7f3ee8eccbcaa
Author: liuxunorg <33...@qq.com>
AuthorDate: Tue Jan 1 01:12:40 2019 +0800

    [ZEPPELIN-3931] Redisplay angularObjectBind when the note is reopened
    
    ### What is this PR for?
    At present, Bind's angularObject in note is only valid in the current operation web page.
    When the note is reopened, or the zeppelin service is restarted, the angularObject of Bind in the note cannot be displayed, and the bind operation must be repeated.
    
    The submarine has a lot of startup commands and parameters. In order to provide the best experience for the user, we provide the parameters to the user through the WEB control.
    
    Zeppelin's own dynamic form is more suitable for parameter input in the query, but it does not meet the needs of the submarine interpreter, so the submarine interpreter uses the angular template to generate a richer input interface.
    The controls in the interface are saved to the note through Bind's angularObject, so that when the user reopens the note, there is no need to re-enter it.
    
    ### What type of PR is it?
    [Improvement]
    
    ### Todos
    * [x] Save angularObject to Note in NotebookServer::angularObjectClientBind(...) function.
    * [x] Delete angularObject to Note in NotebookServer::angularObjectClientUnbind(...) function.
    * [x] Update angularObject to Note in NotebookServer:: angularObjectUpdated(...) function.
    * [x] Load angularObject update to Note in NotebookServer::getNote(...) function.
    * [x] Add test case in NotebookServerTest::testAngularObjectSaveToNote(...).
    * [x] Add test case in NotebookServerTest::testLoadAngularObjectFromNote(...).
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-3931
    
    ### How should this be tested?
    [CI pass](https://travis-ci.org/liuxunorg/zeppelin/builds/473897099)
    
    ### Screenshots (if appropriate)
    
    ![alt text](https://github.com/liuxunorg/images/blob/master/zeppelin/angularBing-save.gif?raw=true "angularBing-save.gif")
    
    ### Questions:
    * Does the licenses files need update? No
    * Is there breaking changes for older versions? No
    * Does this needs documentation? No
    
    Author: liuxunorg <33...@qq.com>
    
    Closes #3278 from liuxunorg/ZEPPELIN-3931 and squashes the following commits:
    
    96cd90b44 [liuxunorg] ### What is this PR for?
---
 .../org/apache/zeppelin/socket/NotebookServer.java |  62 +++++--
 .../apache/zeppelin/socket/NotebookServerTest.java | 161 +++++++++++++++++-
 .../2E1YA3X1U/angularObject_2E1YA3X1U.zpln         | 188 +++++++++++++++++++++
 .../java/org/apache/zeppelin/notebook/Note.java    |  82 +++++++++
 4 files changed, 478 insertions(+), 15 deletions(-)

diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
index e83f26f..21cb0ff 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
@@ -608,8 +608,7 @@ public class NotebookServer extends WebSocketServlet
     return true;
   }
 
-  private void getNote(NotebookSocket conn,
-                       Message fromMessage) throws IOException {
+  private void getNote(NotebookSocket conn, Message fromMessage) throws IOException {
     String noteId = (String) fromMessage.get("id");
     if (noteId == null) {
       return;
@@ -620,11 +619,37 @@ public class NotebookServer extends WebSocketServlet
           public void onSuccess(Note note, ServiceContext context) throws IOException {
             connectionManager.addNoteConnection(note.getId(), conn);
             conn.send(serializeMessage(new Message(OP.NOTE).put("note", note)));
+            updateAngularObjectRegistry(conn, note);
             sendAllAngularObjects(note, context.getAutheInfo().getUser(), conn);
           }
         });
   }
 
+  /**
+   * Update the AngularObject object in the note to InterpreterGroup and AngularObjectRegistry.
+   */
+  private void updateAngularObjectRegistry(NotebookSocket conn, Note note) {
+    for(Paragraph paragraph : note.getParagraphs()) {
+      InterpreterGroup interpreterGroup = null;
+      try {
+        interpreterGroup = findInterpreterGroupForParagraph(note, paragraph.getId());
+      } catch (Exception e) {
+        e.printStackTrace();
+      }
+      RemoteAngularObjectRegistry registry = (RemoteAngularObjectRegistry)
+          interpreterGroup.getAngularObjectRegistry();
+
+      List<AngularObject> angularObjects = note.getAngularObjects(interpreterGroup.getId());
+      for (AngularObject ao : angularObjects) {
+        if (StringUtils.equals(ao.getNoteId(), note.getId())
+            && StringUtils.equals(ao.getParagraphId(), paragraph.getId())) {
+          pushAngularObjectToRemoteRegistry(ao.getNoteId(), ao.getParagraphId(),
+              ao.getName(), ao.get(), registry, interpreterGroup.getId(), conn);
+        }
+      }
+    }
+  }
+
   private void getHomeNote(NotebookSocket conn,
                            Message fromMessage) throws IOException {
 
@@ -1033,7 +1058,8 @@ public class NotebookServer extends WebSocketServlet
   }
 
   /**
-   * When angular object updated from client.
+   * 1. When angular object updated from client.
+   * 2. Save AngularObject to note.
    *
    * @param conn        the web socket.
    * @param fromMessage the message.
@@ -1057,13 +1083,16 @@ public class NotebookServer extends WebSocketServlet
                 new Message(OP.ANGULAR_OBJECT_UPDATE).put("angularObject", ao)
                     .put("interpreterGroupId", interpreterGroupId).put("noteId", noteId)
                     .put("paragraphId", ao.getParagraphId()), conn);
+            Note note = getNotebook().getNote(noteId);
+            note.addOrUpdateAngularObject(interpreterGroupId, ao);
           }
         });
   }
 
   /**
-   * Push the given Angular variable to the target interpreter angular registry given a noteId
-   * and a paragraph id.
+   * 1. Push the given Angular variable to the target interpreter angular
+   *    registry given a noteId and a paragraph id.
+   * 2. Save AngularObject to note.
    */
   protected void angularObjectClientBind(NotebookSocket conn,
                                          Message fromMessage) throws Exception {
@@ -1082,18 +1111,19 @@ public class NotebookServer extends WebSocketServlet
       final InterpreterGroup interpreterGroup = findInterpreterGroupForParagraph(note, paragraphId);
       final RemoteAngularObjectRegistry registry = (RemoteAngularObjectRegistry)
           interpreterGroup.getAngularObjectRegistry();
-      pushAngularObjectToRemoteRegistry(noteId, paragraphId, varName, varValue, registry,
+      AngularObject ao = pushAngularObjectToRemoteRegistry(noteId, paragraphId, varName, varValue, registry,
           interpreterGroup.getId(), conn);
+      note.addOrUpdateAngularObject(interpreterGroup.getId(), ao);
     }
   }
 
   /**
-   * Remove the given Angular variable to the target interpreter(s) angular registry given a noteId
-   * and an optional list of paragraph id(s).
+   * 1. Remove the given Angular variable to the target interpreter(s) angular
+   *    registry given a noteId and an optional list of paragraph id(s).
+   * 2. Delete AngularObject from note.
    */
   protected void angularObjectClientUnbind(NotebookSocket conn,
-                                           Message fromMessage)
-      throws Exception {
+                                           Message fromMessage) throws Exception {
     String noteId = fromMessage.getType("noteId");
     String varName = fromMessage.getType("name");
     String paragraphId = fromMessage.getType("paragraphId");
@@ -1108,9 +1138,9 @@ public class NotebookServer extends WebSocketServlet
       final InterpreterGroup interpreterGroup = findInterpreterGroupForParagraph(note, paragraphId);
       final RemoteAngularObjectRegistry registry = (RemoteAngularObjectRegistry)
           interpreterGroup.getAngularObjectRegistry();
-      removeAngularFromRemoteRegistry(noteId, paragraphId, varName, registry,
+      AngularObject ao = removeAngularFromRemoteRegistry(noteId, paragraphId, varName, registry,
           interpreterGroup.getId(), conn);
-
+      note.deleteAngularObject(interpreterGroup.getId(), ao);
     }
   }
 
@@ -1123,7 +1153,7 @@ public class NotebookServer extends WebSocketServlet
     return paragraph.getBindedInterpreter().getInterpreterGroup();
   }
 
-  private void pushAngularObjectToRemoteRegistry(String noteId, String paragraphId, String varName,
+  private AngularObject pushAngularObjectToRemoteRegistry(String noteId, String paragraphId, String varName,
                                                  Object varValue,
                                                  RemoteAngularObjectRegistry remoteRegistry,
                                                  String interpreterGroupId,
@@ -1135,9 +1165,11 @@ public class NotebookServer extends WebSocketServlet
         .put("angularObject", ao)
         .put("interpreterGroupId", interpreterGroupId).put("noteId", noteId)
         .put("paragraphId", paragraphId), conn);
+
+    return ao;
   }
 
-  private void removeAngularFromRemoteRegistry(String noteId, String paragraphId, String varName,
+  private AngularObject removeAngularFromRemoteRegistry(String noteId, String paragraphId, String varName,
                                                RemoteAngularObjectRegistry remoteRegistry,
                                                String interpreterGroupId,
                                                NotebookSocket conn) {
@@ -1147,6 +1179,8 @@ public class NotebookServer extends WebSocketServlet
         .put("angularObject", ao)
         .put("interpreterGroupId", interpreterGroupId).put("noteId", noteId)
         .put("paragraphId", paragraphId), conn);
+
+    return ao;
   }
 
   private void moveParagraph(NotebookSocket conn,
diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
index 08bed2f..b26110c 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/socket/NotebookServerTest.java
@@ -20,6 +20,7 @@ import static java.util.Arrays.asList;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
 import static org.mockito.Mockito.anyString;
@@ -36,7 +37,6 @@ import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.List;
 import java.util.Map;
-import javax.inject.Provider;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
 import org.apache.zeppelin.display.AngularObject;
@@ -249,6 +249,165 @@ public class NotebookServerTest extends AbstractTestRestApi {
   }
 
   @Test
+  public void testAngularObjectSaveToNote()
+      throws IOException, InterruptedException {
+    // create a notebook
+    Note note1 = notebook.createNote("note1", "angular", anonymous);
+
+    // get reference to interpreterGroup
+    InterpreterGroup interpreterGroup = null;
+    List<InterpreterSetting> settings = notebook.getInterpreterSettingManager()
+        .getInterpreterSettings(note1.getId());
+    for (InterpreterSetting setting : settings) {
+      if (setting.getName().equals("angular")) {
+        interpreterGroup = setting.getOrCreateInterpreterGroup("anonymous", "sharedProcess");
+        break;
+      }
+    }
+
+    // start interpreter process
+    Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+    p1.setText("%angular <h2>Bind here : {{COMMAND_TYPE}}</h2>");
+    p1.setAuthenticationInfo(anonymous);
+    note1.run(p1.getId());
+
+    // wait for paragraph finished
+    while (true) {
+      if (p1.getStatus() == Job.Status.FINISHED) {
+        break;
+      }
+      Thread.sleep(100);
+    }
+    // sleep for 1 second to make sure job running thread finish to fire event. See ZEPPELIN-3277
+    Thread.sleep(1000);
+
+    // create two sockets and open it
+    NotebookSocket sock1 = createWebSocket();
+
+    notebookServer.onOpen(sock1);
+    verify(sock1, times(0)).send(anyString()); // getNote, getAngularObject
+    // open the same notebook from sockets
+    notebookServer.onMessage(sock1, new Message(OP.GET_NOTE).put("id", note1.getId()).toJson());
+
+    reset(sock1);
+
+    // bind object from sock1
+    notebookServer.onMessage(sock1,
+        new Message(OP.ANGULAR_OBJECT_CLIENT_BIND)
+            .put("noteId", note1.getId())
+            .put("paragraphId", p1.getId())
+            .put("name", "COMMAND_TYPE")
+            .put("value", "COMMAND_TYPE_VALUE")
+            .put("interpreterGroupId", interpreterGroup.getId()).toJson());
+    List<AngularObject> list = note1.getAngularObjects("angular-shared_process");
+    assertEquals(list.size(), 1);
+    assertEquals(list.get(0).getNoteId(), note1.getId());
+    assertEquals(list.get(0).getParagraphId(), p1.getId());
+    assertEquals(list.get(0).getName(), "COMMAND_TYPE");
+    assertEquals(list.get(0).get(), "COMMAND_TYPE_VALUE");
+    // Check if the interpreterGroup AngularObjectRegistry is updated
+    Map<String, Map<String, AngularObject>> mapRegistry = interpreterGroup.getAngularObjectRegistry().getRegistry();
+    AngularObject ao = mapRegistry.get(note1.getId()+"_"+p1.getId()).get("COMMAND_TYPE");
+    assertEquals(ao.getName(), "COMMAND_TYPE");
+    assertEquals(ao.get(), "COMMAND_TYPE_VALUE");
+
+    // update bind object from sock1
+    notebookServer.onMessage(sock1,
+        new Message(OP.ANGULAR_OBJECT_UPDATED)
+            .put("noteId", note1.getId())
+            .put("paragraphId", p1.getId())
+            .put("name", "COMMAND_TYPE")
+            .put("value", "COMMAND_TYPE_VALUE_UPDATE")
+            .put("interpreterGroupId", interpreterGroup.getId()).toJson());
+    list = note1.getAngularObjects("angular-shared_process");
+    assertEquals(list.size(), 1);
+    assertEquals(list.get(0).getNoteId(), note1.getId());
+    assertEquals(list.get(0).getParagraphId(), p1.getId());
+    assertEquals(list.get(0).getName(), "COMMAND_TYPE");
+    assertEquals(list.get(0).get(), "COMMAND_TYPE_VALUE_UPDATE");
+    // Check if the interpreterGroup AngularObjectRegistry is updated
+    mapRegistry = interpreterGroup.getAngularObjectRegistry().getRegistry();
+    AngularObject ao1 = mapRegistry.get(note1.getId()+"_"+p1.getId()).get("COMMAND_TYPE");
+    assertEquals(ao1.getName(), "COMMAND_TYPE");
+    assertEquals(ao1.get(), "COMMAND_TYPE_VALUE_UPDATE");
+
+    // unbind object from sock1
+    notebookServer.onMessage(sock1,
+        new Message(OP.ANGULAR_OBJECT_CLIENT_UNBIND)
+            .put("noteId", note1.getId())
+            .put("paragraphId", p1.getId())
+            .put("name", "COMMAND_TYPE")
+            .put("value", "COMMAND_TYPE_VALUE")
+            .put("interpreterGroupId", interpreterGroup.getId()).toJson());
+    list = note1.getAngularObjects("angular-shared_process");
+    assertEquals(list.size(), 0);
+    // Check if the interpreterGroup AngularObjectRegistry is delete
+    mapRegistry = interpreterGroup.getAngularObjectRegistry().getRegistry();
+    AngularObject ao2 = mapRegistry.get(note1.getId()+"_"+p1.getId()).get("COMMAND_TYPE");
+    assertNull(ao2);
+
+    notebook.removeNote(note1.getId(), anonymous);
+  }
+
+  @Test
+  public void testLoadAngularObjectFromNote() throws IOException, InterruptedException {
+    // create a notebook
+    Note note1 = notebook.createNote("note1", anonymous);
+
+    // get reference to interpreterGroup
+    InterpreterGroup interpreterGroup = null;
+    List<InterpreterSetting> settings = notebook.getInterpreterSettingManager()
+        .getInterpreterSettings(note1.getId());
+    for (InterpreterSetting setting : settings) {
+      if (setting.getName().equals("angular")) {
+        interpreterGroup = setting.getOrCreateInterpreterGroup("anonymous", "sharedProcess");
+        break;
+      }
+    }
+
+    // start interpreter process
+    Paragraph p1 = note1.addNewParagraph(AuthenticationInfo.ANONYMOUS);
+    p1.setText("%angular <h2>Bind here : {{COMMAND_TYPE}}</h2>");
+    p1.setAuthenticationInfo(anonymous);
+    note1.run(p1.getId());
+
+    // wait for paragraph finished
+    while (true) {
+      if (p1.getStatus() == Job.Status.FINISHED) {
+        break;
+      }
+      Thread.sleep(100);
+    }
+    // sleep for 1 second to make sure job running thread finish to fire event. See ZEPPELIN-3277
+    Thread.sleep(1000);
+
+    // set note AngularObject
+    AngularObject ao = new AngularObject("COMMAND_TYPE", "COMMAND_TYPE_VALUE", note1.getId(), p1.getId(), null);
+    note1.addOrUpdateAngularObject("angular-shared_process", ao);
+
+    // create sockets and open it
+    NotebookSocket sock1 = createWebSocket();
+    notebookServer.onOpen(sock1);
+
+    // Check the AngularObjectRegistry of the interpreterGroup before executing GET_NOTE
+    Map<String, Map<String, AngularObject>> mapRegistry1 = interpreterGroup.getAngularObjectRegistry().getRegistry();
+    assertEquals(mapRegistry1.size(), 0);
+
+    // open the notebook from sockets, AngularObjectRegistry that triggers the update of the interpreterGroup
+    notebookServer.onMessage(sock1, new Message(OP.GET_NOTE).put("id", note1.getId()).toJson());
+    Thread.sleep(1000);
+
+    // After executing GET_NOTE, check the AngularObjectRegistry of the interpreterGroup
+    Map<String, Map<String, AngularObject>> mapRegistry2 = interpreterGroup.getAngularObjectRegistry().getRegistry();
+    assertEquals(mapRegistry1.size(), 2);
+    AngularObject ao1 = mapRegistry2.get(note1.getId()+"_"+p1.getId()).get("COMMAND_TYPE");
+    assertEquals(ao1.getName(), "COMMAND_TYPE");
+    assertEquals(ao1.get(), "COMMAND_TYPE_VALUE");
+
+    notebook.removeNote(note1.getId(), anonymous);
+  }
+
+  @Test
   public void testImportNotebook() throws IOException {
     String msg = "{\"op\":\"IMPORT_NOTE\",\"data\":" +
         "{\"note\":{\"paragraphs\": [{\"text\": \"Test " +
diff --git a/zeppelin-server/src/test/resources/2E1YA3X1U/angularObject_2E1YA3X1U.zpln b/zeppelin-server/src/test/resources/2E1YA3X1U/angularObject_2E1YA3X1U.zpln
new file mode 100755
index 0000000..fd689a8
--- /dev/null
+++ b/zeppelin-server/src/test/resources/2E1YA3X1U/angularObject_2E1YA3X1U.zpln
@@ -0,0 +1,188 @@
+{
+  "paragraphs": [
+    {
+      "text": "%angular\n\n\u003cform class\u003d\"form-inline\"\u003e\n  \u003cdiv class\u003d\"form-group\"\u003e\n    \u003clabel for\u003d\"paragraphId\"\u003eParagraph Id: \u003c/label\u003e\n    \u003cinput type\u003d\"text\" style\u003d\"width:300px\" class\u003d\"form-control\" id\u003d\"paragraphId\" placeholder\u003d\"Paragraph Id ...\" ng-model\u003d\"paragraph\"\u003e\u003c/input\u003e\n  \u003c/div\u003e\n  \u003cbutton type\u003d\"submit\" class\u003d\"btn btn-primary\" ng- [...]
+      "user": "anonymous",
+      "dateUpdated": "2018-12-30 11:11:13.493",
+      "config": {
+        "tableHide": false,
+        "editorSetting": {
+          "language": "sh",
+          "editOnDblClick": false,
+          "completionSupport": false
+        },
+        "colWidth": 12.0,
+        "editorMode": "ace/mode/undefined",
+        "fontSize": 9.0,
+        "editorHide": false,
+        "runOnSelectionChange": false,
+        "title": true,
+        "results": {},
+        "enabled": true
+      },
+      "settings": {
+        "params": {},
+        "forms": {}
+      },
+      "results": {
+        "code": "SUCCESS",
+        "msg": [
+          {
+            "type": "ANGULAR",
+            "data": "\u003cform class\u003d\"form-inline\"\u003e\n  \u003cdiv class\u003d\"form-group\"\u003e\n    \u003clabel for\u003d\"paragraphId\"\u003eParagraph Id: \u003c/label\u003e\n    \u003cinput type\u003d\"text\" style\u003d\"width:300px\" class\u003d\"form-control\" id\u003d\"paragraphId\" placeholder\u003d\"Paragraph Id ...\" ng-model\u003d\"paragraph\"\u003e\u003c/input\u003e\n  \u003c/div\u003e\n  \u003cbutton type\u003d\"submit\" class\u003d\"btn btn-primary\" ng-click\ [...]
+          }
+        ]
+      },
+      "apps": [],
+      "progressUpdateIntervalMs": 500,
+      "jobName": "paragraph_1545988535211_-832334376",
+      "id": "paragraph_1545878497556_-1717616036",
+      "dateCreated": "2018-12-28 17:15:35.211",
+      "dateStarted": "2018-12-30 11:11:13.678",
+      "dateFinished": "2018-12-30 11:11:13.683",
+      "status": "FINISHED"
+    },
+    {
+      "text": "%angular\n \n\u003ch2\u003ethis : {{COMMAND_TYPE}}\u003c/h2\u003e\n\u003cinput type\u003d\"text\" class\u003d\"form-control\" id\u003d\"paragraphId\" placeholder\u003d\"Paragraph Id ...\" ng-value\u003d\"{{COMMAND_TYPE}}\"\u003e\u003c/input\u003e\n",
+      "user": "anonymous",
+      "dateUpdated": "2018-12-30 14:47:10.849",
+      "config": {
+        "editorSetting": {
+          "language": "sh",
+          "editOnDblClick": false,
+          "completionSupport": false
+        },
+        "colWidth": 12.0,
+        "editorMode": "ace/mode/undefined",
+        "fontSize": 9.0,
+        "runOnSelectionChange": false,
+        "title": true,
+        "results": {},
+        "enabled": true
+      },
+      "settings": {
+        "params": {},
+        "forms": {}
+      },
+      "results": {
+        "code": "SUCCESS",
+        "msg": [
+          {
+            "type": "ANGULAR",
+            "data": "\u003ch2\u003ethis : {{COMMAND_TYPE}}\u003c/h2\u003e\n\u003cinput type\u003d\"text\" class\u003d\"form-control\" id\u003d\"paragraphId\" placeholder\u003d\"Paragraph Id ...\" ng-value\u003d\"{{COMMAND_TYPE}}\"\u003e\u003c/input\u003e"
+          }
+        ]
+      },
+      "apps": [],
+      "progressUpdateIntervalMs": 500,
+      "jobName": "paragraph_1545988535212_-2086109774",
+      "id": "paragraph_1545881601069_1553190230",
+      "dateCreated": "2018-12-28 17:15:35.212",
+      "dateStarted": "2018-12-30 14:47:10.895",
+      "dateFinished": "2018-12-30 14:47:11.690",
+      "status": "FINISHED"
+    },
+    {
+      "text": "%angular\n\n\u003cform class\u003d\"form-inline\"\u003e\n  \u003cdiv class\u003d\"form-group\"\u003e\n    \u003clabel for\u003d\"superheroId\"\u003eSuper Hero: {{COMMAND_TYPE}}\u003c/label\u003e\n    \u003cinput type\u003d\"text\" class\u003d\"form-control\" id\u003d\"superheroId\" placeholder\u003d\"Superhero name ...\" ng-model\u003d\"superhero\"\u003e\u003c/input\u003e\n  \u003c/div\u003e\n  \u003cbutton type\u003d\"submit\" class\u003d\"btn btn-primary\" ng-click\u003d [...]
+      "user": "anonymous",
+      "dateUpdated": "2018-12-30 11:11:17.496",
+      "config": {
+        "editorSetting": {
+          "editOnDblClick": false,
+          "completionSupport": false
+        },
+        "colWidth": 12.0,
+        "editorMode": "ace/mode/undefined",
+        "fontSize": 9.0,
+        "runOnSelectionChange": false,
+        "title": false,
+        "results": {},
+        "enabled": true
+      },
+      "settings": {
+        "params": {},
+        "forms": {}
+      },
+      "results": {
+        "code": "SUCCESS",
+        "msg": [
+          {
+            "type": "ANGULAR",
+            "data": "\u003cform class\u003d\"form-inline\"\u003e\n  \u003cdiv class\u003d\"form-group\"\u003e\n    \u003clabel for\u003d\"superheroId\"\u003eSuper Hero: {{COMMAND_TYPE}}\u003c/label\u003e\n    \u003cinput type\u003d\"text\" class\u003d\"form-control\" id\u003d\"superheroId\" placeholder\u003d\"Superhero name ...\" ng-model\u003d\"superhero\"\u003e\u003c/input\u003e\n  \u003c/div\u003e\n  \u003cbutton type\u003d\"submit\" class\u003d\"btn btn-primary\" ng-click\u003d\"z.an [...]
+          }
+        ]
+      },
+      "apps": [],
+      "progressUpdateIntervalMs": 500,
+      "jobName": "paragraph_1545988535212_711026158",
+      "id": "paragraph_1545881216236_725482559",
+      "dateCreated": "2018-12-28 17:15:35.212",
+      "dateStarted": "2018-12-30 11:11:17.535",
+      "dateFinished": "2018-12-30 11:11:17.539",
+      "status": "FINISHED"
+    },
+    {
+      "text": "%angular\n\u003cform class\u003d\"form-inline\"\u003e\n  \u003cdiv class\u003d\"form-group\"\u003e\n    \u003clabel for\u003d\"superheroId\"\u003eSuper Hero: \u003c/label\u003e\n    \u003cinput type\u003d\"text\" class\u003d\"form-control\" id\u003d\"superheroId\" placeholder\u003d\"Superhero name ...\" ng-model\u003d\"superhero\"\u003e\u003c/input\u003e\n  \u003c/div\u003e\n  \u003cbutton type\u003d\"submit\" class\u003d\"btn btn-primary\" ng-click\u003d\"z.angularBind(\u [...]
+      "user": "anonymous",
+      "dateUpdated": "2018-12-28 21:58:58.034",
+      "config": {
+        "colWidth": 12.0,
+        "fontSize": 9.0,
+        "enabled": true,
+        "results": {},
+        "editorSetting": {
+          "editOnDblClick": false,
+          "completionSupport": false
+        },
+        "editorMode": "ace/mode/undefined"
+      },
+      "settings": {
+        "params": {},
+        "forms": {}
+      },
+      "apps": [],
+      "progressUpdateIntervalMs": 500,
+      "jobName": "paragraph_1545988714036_1708666246",
+      "id": "paragraph_1545988714036_1708666246",
+      "dateCreated": "2018-12-28 17:18:34.036",
+      "status": "READY"
+    }
+  ],
+  "name": "angularObject",
+  "id": "2E1YA3X1U",
+  "defaultInterpreterGroup": "angular",
+  "noteParams": {},
+  "noteForms": {},
+  "angularObjects": {
+    "angular-shared_process": [
+      {
+        "name": "COMMAND_TYPE",
+        "object": "333",
+        "noteId": "2E1YA3X1U",
+        "paragraphId": "paragraph_1545881601069_1553190230"
+      },
+      {
+        "name": "COMMAND_TYPE",
+        "object": "333",
+        "noteId": "2E1YA3X1U",
+        "paragraphId": "paragraph_1545881601069_1553190230"
+      },
+      {
+        "name": "COMMAND_TYPE",
+        "object": "4",
+        "noteId": "2E1YA3X1U",
+        "paragraphId": "paragraph_1545881601069_1553190230"
+      },
+      {
+        "name": "COMMAND_TYPE",
+        "object": "111",
+        "noteId": "2E1YA3X1U",
+        "paragraphId": "paragraph_1545881601069_1553190230"
+      }
+    ]
+  },
+  "config": {
+    "isZeppelinNotebookCronEnable": false
+  },
+  "info": {}
+}
\ No newline at end of file
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 a583c82..403db06 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
@@ -31,6 +31,7 @@ import org.apache.zeppelin.interpreter.InterpreterGroup;
 import org.apache.zeppelin.interpreter.InterpreterResult;
 import org.apache.zeppelin.interpreter.InterpreterSetting;
 import org.apache.zeppelin.interpreter.InterpreterSettingManager;
+import org.apache.zeppelin.interpreter.remote.RemoteAngularObject;
 import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry;
 import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
 import org.apache.zeppelin.notebook.utility.IdHashes;
@@ -289,6 +290,87 @@ public class Note implements JsonSerializable {
     return angularObjects;
   }
 
+  public List<AngularObject> getAngularObjects(String intpGroupId) {
+    if (!angularObjects.containsKey(intpGroupId)) {
+      return new ArrayList<>();
+    }
+    return angularObjects.get(intpGroupId);
+  }
+
+  /**
+   * Add or update the note AngularObject.
+   */
+  public void addOrUpdateAngularObject(String intpGroupId, AngularObject angularObject) {
+    List<AngularObject> angularObjectList;
+    if (!angularObjects.containsKey(intpGroupId)) {
+      angularObjectList = new ArrayList<>();
+      angularObjects.put(intpGroupId, angularObjectList);
+    } else {
+      angularObjectList = angularObjects.get(intpGroupId);
+
+      // Delete existing AngularObject
+      Iterator<AngularObject> iter = angularObjectList.iterator();
+      while(iter.hasNext()){
+        String noteId = "", paragraphId = "", name = "";
+        Object object = iter.next();
+        if (object instanceof AngularObject) {
+          AngularObject ao = (AngularObject)object;
+          noteId = ao.getNoteId();
+          paragraphId = ao.getParagraphId();
+          name = ao.getName();
+        } else if (object instanceof RemoteAngularObject) {
+          RemoteAngularObject rao = (RemoteAngularObject)object;
+          noteId = rao.getNoteId();
+          paragraphId = rao.getParagraphId();
+          name = rao.getName();
+        } else {
+          continue;
+        }
+        if (StringUtils.equals(noteId, angularObject.getNoteId())
+            && StringUtils.equals(paragraphId, angularObject.getParagraphId())
+            && StringUtils.equals(name, angularObject.getName())) {
+          iter.remove();
+        }
+      }
+    }
+
+    angularObjectList.add(angularObject);
+  }
+
+  /**
+   * Delete the note AngularObject.
+   */
+  public void deleteAngularObject(String intpGroupId, AngularObject angularObject) {
+    List<AngularObject> angularObjectList;
+    if (!angularObjects.containsKey(intpGroupId)) {
+      return;
+    } else {
+      angularObjectList = angularObjects.get(intpGroupId);
+
+      // Delete existing AngularObject
+      Iterator<AngularObject> iter = angularObjectList.iterator();
+      while(iter.hasNext()){
+        String noteId = "", paragraphId = "";
+        Object object = iter.next();
+        if (object instanceof AngularObject) {
+          AngularObject ao = (AngularObject)object;
+          noteId = ao.getNoteId();
+          paragraphId = ao.getParagraphId();
+        } else if (object instanceof RemoteAngularObject) {
+          RemoteAngularObject rao = (RemoteAngularObject)object;
+          noteId = rao.getNoteId();
+          paragraphId = rao.getParagraphId();
+        } else {
+          continue;
+        }
+        if (StringUtils.equals(noteId, angularObject.getNoteId())
+            && StringUtils.equals(paragraphId, angularObject.getParagraphId())) {
+          iter.remove();
+        }
+      }
+    }
+  }
+
   /**
    * Create a new paragraph and add it to the end of the note.
    */