You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by mo...@apache.org on 2015/12/27 20:27:40 UTC

incubator-zeppelin git commit: ZEPPELIN-511 REST API: Insert / Retrieve / Move / Delete operation for paragraph

Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master d64920d20 -> 3bfd97e23


ZEPPELIN-511 REST API: Insert / Retrieve / Move / Delete operation for paragraph

### What is this PR for?

This issue is intended to fill gap between REST API and WebSocket operations.
For now we can only access notebook and paragraph to READONLY (not writing) via REST API but after this PR, we can insert / retrieve / move / delete paragraph via REST API.

### What type of PR is it?

Feature

### Todos

### Is there a relevant Jira issue?

https://issues.apache.org/jira/browse/ZEPPELIN-511

### How should this be tested?

Please follow the explanation of added REST APIs from rest-notebook.md.

### Screenshots (if appropriate)

### Questions:
* Does the licenses files need update? (No)
* Is there breaking changes for older versions? (No)
* Does this needs documentation? (Yes, I've addressed it.)

Author: Jungtaek Lim <ka...@gmail.com>

Closes #550 from HeartSaVioR/ZEPPELIN-511 and squashes the following commits:

59f5f61 [Jungtaek Lim] ZEPPELIN-511 Address @prabhjyotsingh reviews
01feed4 [Jungtaek Lim] ZEPPELIN-511 explain new APIs to read-notebook.md
cc71e06 [Jungtaek Lim] ZEPPELIN-511 "insert paragraph" api: return created paragraph id
3658b00 [Jungtaek Lim] ZEPPELIN-511 REST API: Insert / Retrieve / Move / Delete operation for paragraph


Project: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/commit/3bfd97e2
Tree: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/tree/3bfd97e2
Diff: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/diff/3bfd97e2

Branch: refs/heads/master
Commit: 3bfd97e236b66675c9da96f7ab00c45f053bd887
Parents: d64920d
Author: Jungtaek Lim <ka...@gmail.com>
Authored: Mon Dec 21 16:16:05 2015 +0900
Committer: Lee moon soo <mo...@apache.org>
Committed: Sun Dec 27 11:29:20 2015 -0800

----------------------------------------------------------------------
 docs/rest-api/rest-notebook.md                  | 187 +++++++++++++++++++
 .../apache/zeppelin/rest/NotebookRestApi.java   | 124 +++++++++++-
 .../rest/message/NewParagraphRequest.java       |   8 +-
 .../zeppelin/rest/ZeppelinRestApiTest.java      | 122 ++++++++++++
 .../java/org/apache/zeppelin/notebook/Note.java |  34 ++--
 5 files changed, 460 insertions(+), 15 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/3bfd97e2/docs/rest-api/rest-notebook.md
----------------------------------------------------------------------
diff --git a/docs/rest-api/rest-notebook.md b/docs/rest-api/rest-notebook.md
index ffc3d8d..3c94268 100644
--- a/docs/rest-api/rest-notebook.md
+++ b/docs/rest-api/rest-notebook.md
@@ -586,3 +586,190 @@ limitations under the License.
     </tr>
   </table>
   
+<br/>
+
+
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Create paragraph</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```POST``` method create a new paragraph using JSON payload.
+          The body field of the returned JSON contain the new paragraph id.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[notebookId]/paragraph```</td>
+    </tr>
+    <tr>
+      <td>Success code</td>
+      <td>201</td>
+    </tr>
+    <tr>
+      <td> Fail code</td>
+      <td> 500 </td>
+    </tr>
+    <tr>
+      <td> sample JSON input (add to the last) </td>
+      <td><pre>
+  { 
+    "title": "Paragraph insert revised", 
+    "text": "%spark\nprintln(\"Paragraph insert revised\")" 
+  }</pre></td>
+    </tr>
+    <tr>
+      <td> sample JSON input (add to specific index) </td>
+      <td><pre>
+  { 
+    "title": "Paragraph insert revised", 
+    "text": "%spark\nprintln(\"Paragraph insert revised\")",
+    "index": 0
+  }
+      </pre></td>
+    </tr>
+    <tr>
+      <td> sample JSON response </td>
+      <td><pre>{"status": "CREATED","message": "","body": "20151218-100330_1754029574"}</pre></td>
+    </tr>
+  </table>
+  
+<br/>
+
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Get paragraph</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```GET``` method retrieves an existing paragraph's information using the given id.
+          The body field of the returned JSON contain information about paragraph.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[notebookId]/paragraph/[paragraphId]```</td>
+    </tr>
+    <tr>
+      <td>Success code</td>
+      <td>200</td>
+    </tr>
+    <tr>
+      <td> Fail code</td>
+      <td> 500 </td>
+    </tr>
+    <tr>
+      <td> sample JSON response </td>
+      <td><pre>
+{
+  "status": "OK",
+  "message": "",
+  "body": {
+    "title": "Paragraph2",
+    "text": "%spark\n\nprintln(\"it's paragraph2\")",
+    "dateUpdated": "Dec 18, 2015 7:33:54 AM",
+    "config": {
+      "colWidth": 12,
+      "graph": {
+        "mode": "table",
+        "height": 300,
+        "optionOpen": false,
+        "keys": [],
+        "values": [],
+        "groups": [],
+        "scatter": {}
+      },
+      "enabled": true,
+      "title": true,
+      "editorMode": "ace/mode/scala"
+    },
+    "settings": {
+      "params": {},
+      "forms": {}
+    },
+    "jobName": "paragraph_1450391574392_-1890856722",
+    "id": "20151218-073254_1105602047",
+    "result": {
+      "code": "SUCCESS",
+      "type": "TEXT",
+      "msg": "it's paragraph2\n"
+    },
+    "dateCreated": "Dec 18, 2015 7:32:54 AM",
+    "dateStarted": "Dec 18, 2015 7:33:55 AM",
+    "dateFinished": "Dec 18, 2015 7:33:55 AM",
+    "status": "FINISHED",
+    "progressUpdateIntervalMs": 500
+  }
+}
+      </pre></td>
+    </tr>
+  </table>
+  
+<br/>
+
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Move paragraph</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```POST``` method moves a paragraph to the specific index (order) from the notebook.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[notebookId]/paragraph/[paragraphId]/move/[newIndex]```</td>
+    </tr>
+    <tr>
+      <td>Success code</td>
+      <td>200</td>
+    </tr>
+    <tr>
+      <td> Fail code</td>
+      <td> 500 </td>
+    </tr>
+    <tr>
+      <td> sample JSON response </td>
+      <td><pre>{"status":"OK","message":""}</pre></td>
+    </tr>
+  </table>
+  
+
+<br/>
+
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Delete paragraph</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```DELETE``` method deletes a paragraph by the given notebook and paragraph id.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[notebookId]/paragraph/[paragraphId]```</td>
+    </tr>
+    <tr>
+      <td>Success code</td>
+      <td>200</td>
+    </tr>
+    <tr>
+      <td> Fail code</td>
+      <td> 500 </td>
+    </tr>
+    <tr>
+      <td> sample JSON response </td>
+      <td><pre>{"status":"OK","message":""}</pre></td>
+    </tr>
+  </table>
+  

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/3bfd97e2/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
index fb4e994..aefa2c1 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/NotebookRestApi.java
@@ -218,7 +218,129 @@ public class NotebookRestApi {
     notebookServer.broadcastNoteList();
     return new JsonResponse<>(Status.CREATED, "", newNote.getId()).build();
   }
-  
+
+  /**
+   * Insert paragraph REST API
+   * @param message - JSON containing paragraph's information
+   * @return JSON with status.OK
+   * @throws IOException
+   */
+  @POST
+  @Path("{notebookId}/paragraph")
+  public Response insertParagraph(@PathParam("notebookId") String notebookId, String message)
+      throws IOException {
+    LOG.info("insert paragraph {} {}", notebookId, message);
+
+    Note note = notebook.getNote(notebookId);
+    if (note == null) {
+      return new JsonResponse(Status.NOT_FOUND, "note not found.").build();
+    }
+
+    NewParagraphRequest request = gson.fromJson(message, NewParagraphRequest.class);
+
+    Paragraph p;
+    Double indexDouble = request.getIndex();
+    if (indexDouble == null) {
+      p = note.addParagraph();
+    } else {
+      p = note.insertParagraph(indexDouble.intValue());
+    }
+    p.setTitle(request.getTitle());
+    p.setText(request.getText());
+
+    note.persist();
+    notebookServer.broadcastNote(note);
+    return new JsonResponse(Status.CREATED, "", p.getId()).build();
+  }
+
+  /**
+   * Get paragraph REST API
+   * @param
+   * @return JSON with information of the paragraph
+   * @throws IOException
+   */
+  @GET
+  @Path("{notebookId}/paragraph/{paragraphId}")
+  public Response getParagraph(@PathParam("notebookId") String notebookId,
+                               @PathParam("paragraphId") String paragraphId) throws IOException {
+    LOG.info("get paragraph {} {}", notebookId, paragraphId);
+
+    Note note = notebook.getNote(notebookId);
+    if (note == null) {
+      return new JsonResponse(Status.NOT_FOUND, "note not found.").build();
+    }
+
+    Paragraph p = note.getParagraph(paragraphId);
+    if (p == null) {
+      return new JsonResponse(Status.NOT_FOUND, "paragraph not found.").build();
+    }
+
+    return new JsonResponse(Status.OK, "", p).build();
+  }
+
+  /**
+   * Move paragraph REST API
+   * @param newIndex - new index to move
+   * @return JSON with status.OK
+   * @throws IOException
+   */
+  @POST
+  @Path("{notebookId}/paragraph/{paragraphId}/move/{newIndex}")
+  public Response moveParagraph(@PathParam("notebookId") String notebookId,
+                                @PathParam("paragraphId") String paragraphId,
+                                @PathParam("newIndex") String newIndex) throws IOException {
+    LOG.info("move paragraph {} {} {}", notebookId, paragraphId, newIndex);
+
+    Note note = notebook.getNote(notebookId);
+    if (note == null) {
+      return new JsonResponse(Status.NOT_FOUND, "note not found.").build();
+    }
+
+    Paragraph p = note.getParagraph(paragraphId);
+    if (p == null) {
+      return new JsonResponse(Status.NOT_FOUND, "paragraph not found.").build();
+    }
+
+    try {
+      note.moveParagraph(paragraphId, Integer.parseInt(newIndex), true);
+
+      note.persist();
+      notebookServer.broadcastNote(note);
+      return new JsonResponse(Status.OK, "").build();
+    } catch (IndexOutOfBoundsException e) {
+      return new JsonResponse(Status.BAD_REQUEST, "paragraph's new index is out of bound").build();
+    }
+  }
+
+  /**
+   * Delete paragraph REST API
+   * @param
+   * @return JSON with status.OK
+   * @throws IOException
+   */
+  @DELETE
+  @Path("{notebookId}/paragraph/{paragraphId}")
+  public Response deleteParagraph(@PathParam("notebookId") String notebookId,
+                                  @PathParam("paragraphId") String paragraphId) throws IOException {
+    LOG.info("delete paragraph {} {}", notebookId, paragraphId);
+
+    Note note = notebook.getNote(notebookId);
+    if (note == null) {
+      return new JsonResponse(Status.NOT_FOUND, "note not found.").build();
+    }
+
+    Paragraph p = note.getParagraph(paragraphId);
+    if (p == null) {
+      return new JsonResponse(Status.NOT_FOUND, "paragraph not found.").build();
+    }
+
+    note.removeParagraph(paragraphId);
+    note.persist();
+    notebookServer.broadcastNote(note);
+
+    return new JsonResponse(Status.OK, "").build();
+  }
+
   /**
    * Run notebook jobs REST API
    * @param

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/3bfd97e2/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewParagraphRequest.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewParagraphRequest.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewParagraphRequest.java
index a1d6556..bde920b 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewParagraphRequest.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewParagraphRequest.java
@@ -20,12 +20,12 @@ package org.apache.zeppelin.rest.message;
 /**
  * NewParagraphRequest rest api request message
  *
- * It is used for NewNotebookRequest with initial paragraphs
- *
+ * index field will be ignored when it's used to provide initial paragraphs
  */
 public class NewParagraphRequest {
   String title;
   String text;
+  Double index;
 
   public NewParagraphRequest() {
 
@@ -38,4 +38,8 @@ public class NewParagraphRequest {
   public String getText() {
     return text;
   }
+
+  public Double getIndex() {
+    return index;
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/3bfd97e2/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
index c69b223..0fde5bf 100644
--- a/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
+++ b/zeppelin-server/src/test/java/org/apache/zeppelin/rest/ZeppelinRestApiTest.java
@@ -602,5 +602,127 @@ public class ZeppelinRestApiTest extends AbstractTestRestApi {
 
     ZeppelinServer.notebook.removeNote(note.getId());
   }
+
+  @Test
+  public void testInsertParagraph() throws IOException {
+    Note note = ZeppelinServer.notebook.createNote();
+
+    String jsonRequest = "{\"title\": \"title1\", \"text\": \"text1\"}";
+    PostMethod post = httpPost("/notebook/" + note.getId() + "/paragraph", jsonRequest);
+    LOG.info("testInsertParagraph response\n" + post.getResponseBodyAsString());
+    assertThat("Test insert method:", post, isCreated());
+    post.releaseConnection();
+
+    Map<String, Object> resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken<Map<String, Object>>() {
+    }.getType());
+
+    String newParagraphId = (String) resp.get("body");
+    LOG.info("newParagraphId:=" + newParagraphId);
+
+    Note retrNote = ZeppelinServer.notebook.getNote(note.getId());
+    Paragraph newParagraph = retrNote.getParagraph(newParagraphId);
+    assertNotNull("Can not find new paragraph by id", newParagraph);
+
+    assertEquals("title1", newParagraph.getTitle());
+    assertEquals("text1", newParagraph.getText());
+
+    Paragraph lastParagraph = note.getLastParagraph();
+    assertEquals(newParagraph.getId(), lastParagraph.getId());
+
+    // insert to index 0
+    String jsonRequest2 = "{\"index\": 0, \"title\": \"title2\", \"text\": \"text2\"}";
+    PostMethod post2 = httpPost("/notebook/" + note.getId() + "/paragraph", jsonRequest2);
+    LOG.info("testInsertParagraph response2\n" + post2.getResponseBodyAsString());
+    assertThat("Test insert method:", post2, isCreated());
+    post2.releaseConnection();
+
+    Paragraph paragraphAtIdx0 = note.getParagraphs().get(0);
+    assertEquals("title2", paragraphAtIdx0.getTitle());
+    assertEquals("text2", paragraphAtIdx0.getText());
+
+    ZeppelinServer.notebook.removeNote(note.getId());
+  }
+
+  @Test
+  public void testGetParagraph() throws IOException {
+    Note note = ZeppelinServer.notebook.createNote();
+
+    Paragraph p = note.addParagraph();
+    p.setTitle("hello");
+    p.setText("world");
+    note.persist();
+
+    GetMethod get = httpGet("/notebook/" + note.getId() + "/paragraph/" + p.getId());
+    LOG.info("testGetParagraph response\n" + get.getResponseBodyAsString());
+    assertThat("Test get method: ", get, isAllowed());
+    get.releaseConnection();
+
+    Map<String, Object> resp = gson.fromJson(get.getResponseBodyAsString(), new TypeToken<Map<String, Object>>() {
+    }.getType());
+
+    assertNotNull(resp);
+    assertEquals("OK", resp.get("status"));
+
+    Map<String, Object> body = (Map<String, Object>) resp.get("body");
+
+    assertEquals(p.getId(), body.get("id"));
+    assertEquals("hello", body.get("title"));
+    assertEquals("world", body.get("text"));
+
+    ZeppelinServer.notebook.removeNote(note.getId());
+  }
+
+  @Test
+  public void testMoveParagraph() throws IOException {
+    Note note = ZeppelinServer.notebook.createNote();
+
+    Paragraph p = note.addParagraph();
+    p.setTitle("title1");
+    p.setText("text1");
+
+    Paragraph p2 = note.addParagraph();
+    p2.setTitle("title2");
+    p2.setText("text2");
+
+    note.persist();
+
+    PostMethod post = httpPost("/notebook/" + note.getId() + "/paragraph/" + p2.getId() + "/move/" + 0, "");
+    assertThat("Test post method: ", post, isAllowed());
+    post.releaseConnection();
+
+    Note retrNote = ZeppelinServer.notebook.getNote(note.getId());
+    Paragraph paragraphAtIdx0 = retrNote.getParagraphs().get(0);
+
+    assertEquals(p2.getId(), paragraphAtIdx0.getId());
+    assertEquals(p2.getTitle(), paragraphAtIdx0.getTitle());
+    assertEquals(p2.getText(), paragraphAtIdx0.getText());
+
+    PostMethod post2 = httpPost("/notebook/" + note.getId() + "/paragraph/" + p2.getId() + "/move/" + 10, "");
+    assertThat("Test post method: ", post2, isBadRequest());
+    post.releaseConnection();
+
+    ZeppelinServer.notebook.removeNote(note.getId());
+  }
+
+  @Test
+  public void testDeleteParagraph() throws IOException {
+    Note note = ZeppelinServer.notebook.createNote();
+
+    Paragraph p = note.addParagraph();
+    p.setTitle("title1");
+    p.setText("text1");
+
+    note.persist();
+
+    DeleteMethod delete = httpDelete("/notebook/" + note.getId() + "/paragraph/" + p.getId());
+    assertThat("Test delete method: ", delete, isAllowed());
+    delete.releaseConnection();
+
+    Note retrNote = ZeppelinServer.notebook.getNote(note.getId());
+    Paragraph retrParagrah = retrNote.getParagraph(p.getId());
+    assertNull("paragraph should be deleted", retrParagrah);
+
+    ZeppelinServer.notebook.removeNote(note.getId());
+  }
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/3bfd97e2/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Note.java
----------------------------------------------------------------------
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 47da7c8..392b968 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
@@ -49,10 +49,9 @@ import com.google.gson.Gson;
  * Binded interpreters for a note
  */
 public class Note implements Serializable, JobListener {
-  transient Logger logger = LoggerFactory.getLogger(Note.class);
   private static final long serialVersionUID = 7920699076577612429L;
 
-  List<Paragraph> paragraphs = new LinkedList<>();
+  final List<Paragraph> paragraphs = new LinkedList<>();
   private String name = "";
   private String id;
 
@@ -245,12 +244,29 @@ public class Note implements Serializable, JobListener {
    * @param index new index
    */
   public void moveParagraph(String paragraphId, int index) {
+    moveParagraph(paragraphId, index, false);
+  }
+
+  /**
+   * Move paragraph into the new index (order from 0 ~ n-1).
+   *
+   * @param paragraphId
+   * @param index new index
+   * @param throwWhenIndexIsOutOfBound whether throw IndexOutOfBoundException
+   *                                   when index is out of bound
+   */
+  public void moveParagraph(String paragraphId, int index, boolean throwWhenIndexIsOutOfBound) {
     synchronized (paragraphs) {
-      int oldIndex = -1;
+      int oldIndex;
       Paragraph p = null;
 
       if (index < 0 || index >= paragraphs.size()) {
-        return;
+        if (throwWhenIndexIsOutOfBound) {
+          throw new IndexOutOfBoundsException("paragraph size is " + paragraphs.size() +
+              " , index is " + index);
+        } else {
+          return;
+        }
       }
 
       for (int i = 0; i < paragraphs.size(); i++) {
@@ -263,14 +279,8 @@ public class Note implements Serializable, JobListener {
         }
       }
 
-      if (p == null) {
-        return;
-      } else {
-        if (oldIndex < index) {
-          paragraphs.add(index, p);
-        } else {
-          paragraphs.add(index, p);
-        }
+      if (p != null) {
+        paragraphs.add(index, p);
       }
     }
   }