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/10/14 11:19:56 UTC

incubator-zeppelin git commit: zeppelin-333:Notebook create, delete & clone REST API

Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master 4b8512bcf -> 47df1cd1d


zeppelin-333:Notebook create, delete & clone REST API

Initial implementation of createNote REST API.
implementation overlap socket implementation, to minimal the effect for socket behavior, later PR can refactor the creation "logic" (create note, add paragraph, set name) to the zengine class and avoid duplication or even better migrate web client to use REST API as well.
Still need to find a way to broadcast note list to all client same as socket does in
```broadcastNote(note);``` and ```broadcastNoteList();```

Author: eranwitkon <go...@gmail.com>

Closes #334 from eranwitkon/333 and squashes the following commits:

7eb28a0 [eranwitkon] cleanup TODO
aa23d31 [eranwitkon] Add cleanup for test of clone REST API
18f7001 [eranwitkon] Add support for clone Notebook Rest API
defef05 [eranwitkon] refactor clone note to use zengine.notebook.cloneNote and expose brodcast method as public
80b885a [eranwitkon] Add Clone existing note to zengine server
6a8b569 [eranwitkon] initial implementation of Delete notebook REST API
f61bb8c [eranwitkon] persist note after create to save new paragraph and new name
310b449 [eranwitkon] Add access to Notebook socket to the NotebookRest API to allow broadcast messages on notebooks changes.
d1efbab [eranwitkon] fix typo
d957fa2 [eranwitkon] add documentation for notebook create, clone & delete REST API
69b3882 [eranwitkon] initial implementation if createNote REST API


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

Branch: refs/heads/master
Commit: 47df1cd1de5555ed373d8699d62ab3069a0ab890
Parents: 4b8512b
Author: eranwitkon <go...@gmail.com>
Authored: Sun Oct 4 01:51:51 2015 +0300
Committer: Lee moon soo <mo...@apache.org>
Committed: Wed Oct 14 11:20:40 2015 +0200

----------------------------------------------------------------------
 docs/docs/index.md                              |   2 +-
 docs/docs/rest-api/rest-interpreter.md          |   2 +-
 .../rest-json-notebook-create-response.json     |   1 +
 .../rest-json/rest-json-notebook-create.json    |   1 +
 .../rest-json-notebook-delete-response.json     |   1 +
 docs/docs/rest-api/rest-notebook.md             | 126 +++++++++++++++++++
 .../apache/zeppelin/rest/NotebookRestApi.java   |  86 ++++++++++++-
 .../rest/message/NewNotebookRequest.java        |  38 ++++++
 .../apache/zeppelin/server/ZeppelinServer.java  |   2 +-
 .../apache/zeppelin/socket/NotebookServer.java  |  21 +---
 .../zeppelin/rest/ZeppelinRestApiTest.java      | 109 +++++++++++++++-
 .../org/apache/zeppelin/notebook/Notebook.java  |  31 +++++
 12 files changed, 389 insertions(+), 31 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/docs/docs/index.md
----------------------------------------------------------------------
diff --git a/docs/docs/index.md b/docs/docs/index.md
index d243c41..1424fa1 100644
--- a/docs/docs/index.md
+++ b/docs/docs/index.md
@@ -44,7 +44,7 @@ group: nav-right
 
 ### REST API
  * [Interpreter API](./rest-api/rest-interpreter.html)
- * [Notebook API](../docs/pleasecontribute.html)
+ * [Notebook API](./rest-api/rest-notebook.html)
 
 ### Development
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/docs/docs/rest-api/rest-interpreter.md
----------------------------------------------------------------------
diff --git a/docs/docs/rest-api/rest-interpreter.md b/docs/docs/rest-api/rest-interpreter.md
index 8bd56a0..e3f27ca 100644
--- a/docs/docs/rest-api/rest-interpreter.md
+++ b/docs/docs/rest-api/rest-interpreter.md
@@ -11,7 +11,7 @@ group: rest-api
  
  All REST API are available starting with the following endpoint ```http://[zeppelin-server]:[zeppelin-port]/api```
  
- Note that zeppein REST API receive or return JSON objects, it it recomended you install some JSON view such as 
+ Note that zeppein REST API receive or return JSON objects, it it recommended you install some JSON view such as 
  [JSONView](https://chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc)
  
  

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/docs/docs/rest-api/rest-json/rest-json-notebook-create-response.json
----------------------------------------------------------------------
diff --git a/docs/docs/rest-api/rest-json/rest-json-notebook-create-response.json b/docs/docs/rest-api/rest-json/rest-json-notebook-create-response.json
new file mode 100644
index 0000000..acb89df
--- /dev/null
+++ b/docs/docs/rest-api/rest-json/rest-json-notebook-create-response.json
@@ -0,0 +1 @@
+{"status": "CREATED","message": "","body": "2AZPHY918"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/docs/docs/rest-api/rest-json/rest-json-notebook-create.json
----------------------------------------------------------------------
diff --git a/docs/docs/rest-api/rest-json/rest-json-notebook-create.json b/docs/docs/rest-api/rest-json/rest-json-notebook-create.json
new file mode 100644
index 0000000..cd67820
--- /dev/null
+++ b/docs/docs/rest-api/rest-json/rest-json-notebook-create.json
@@ -0,0 +1 @@
+{"name": "name of new notebook"}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/docs/docs/rest-api/rest-json/rest-json-notebook-delete-response.json
----------------------------------------------------------------------
diff --git a/docs/docs/rest-api/rest-json/rest-json-notebook-delete-response.json b/docs/docs/rest-api/rest-json/rest-json-notebook-delete-response.json
new file mode 100644
index 0000000..a2b9b29
--- /dev/null
+++ b/docs/docs/rest-api/rest-json/rest-json-notebook-delete-response.json
@@ -0,0 +1 @@
+{"status":"OK","message":""}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/docs/docs/rest-api/rest-notebook.md
----------------------------------------------------------------------
diff --git a/docs/docs/rest-api/rest-notebook.md b/docs/docs/rest-api/rest-notebook.md
new file mode 100644
index 0000000..1c367ce
--- /dev/null
+++ b/docs/docs/rest-api/rest-notebook.md
@@ -0,0 +1,126 @@
+---
+layout: page
+title: "Notebook REST API"
+description: ""
+group: rest-api
+---
+{% include JB/setup %}
+
+## Zeppelin REST API
+ Zeppelin provides several REST API's for interaction and remote activation of zeppelin functionality.
+ 
+ All REST API are available starting with the following endpoint ```http://[zeppelin-server]:[zeppelin-port]/api```
+ 
+ Note that zeppein REST API receive or return JSON objects, it it recommended you install some JSON view such as 
+ [JSONView](https://chrome.google.com/webstore/detail/jsonview/chklaanhfefbnpoihckbnefhakgolnmc)
+ 
+ 
+ If you work with zeppelin and find a need for an additional REST API please [file an issue or send us mail](../../community.html) 
+
+ <br />
+### Notebook REST API list
+  
+  Notebooks can be created, deleted or cloned using the following REST API
+  
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Create notebook</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```POST``` method create a new notebook using the given name or default name if none given.
+          The body field of the returned JSON contain the new notebook id.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook```</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 </td>
+      <td> [Create JSON sample](rest-json/rest-json-notebook-create.json)</td>
+    </tr>
+    <tr>
+      <td> sample JSON response </td>
+      <td> [Create response sample](rest-json/rest-json-notebook-create-response.json) </td>
+    </tr>
+  </table>
+  
+<br/>
+
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Delete notebook</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```DELETE``` method delete a notebook by the given notebook id.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[notebookId]```</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> [Delete response sample](rest-json/rest-json-notebook-delete-response.json) </td>
+    </tr>
+  </table>
+  
+<br/>
+  
+  <table class="table-configuration">
+    <col width="200">
+    <tr>
+      <th>Clone notebook</th>
+      <th></th>
+    </tr>
+    <tr>
+      <td>Description</td>
+      <td>This ```POST``` method clone a notebook by the given id and create a new notebook using the given name 
+          or default name if none given.
+          The body field of the returned JSON contain the new notebook id.
+      </td>
+    </tr>
+    <tr>
+      <td>URL</td>
+      <td>```http://[zeppelin-server]:[zeppelin-port]/api/notebook/[notebookId]```</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 </td>
+      <td> [Clone JSON sample](rest-json/rest-json-notebook-create.json)</td>
+    </tr>
+    <tr>
+      <td> sample JSON response </td>
+      <td> [Clone response sample](rest-json/rest-json-notebook-create-response.json) </td>
+    </tr>
+  </table>
+  

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/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 8a933f7..aabc8c6 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
@@ -21,18 +21,19 @@ import java.io.IOException;
 import java.util.LinkedList;
 import java.util.List;
 
-import javax.ws.rs.GET;
-import javax.ws.rs.PUT;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.Produces;
+import javax.ws.rs.*;
 import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 
 import org.apache.zeppelin.interpreter.InterpreterSetting;
+import org.apache.zeppelin.notebook.Note;
 import org.apache.zeppelin.notebook.Notebook;
 import org.apache.zeppelin.rest.message.InterpreterSettingListForNoteBind;
+import org.apache.zeppelin.rest.message.NewInterpreterSettingRequest;
+import org.apache.zeppelin.rest.message.NewNotebookRequest;
 import org.apache.zeppelin.server.JsonResponse;
+import org.apache.zeppelin.server.ZeppelinServer;
+import org.apache.zeppelin.socket.NotebookServer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -48,11 +49,14 @@ public class NotebookRestApi {
   Logger logger = LoggerFactory.getLogger(NotebookRestApi.class);
   Gson gson = new Gson();
   private Notebook notebook;
+  private NotebookServer notebookServer;
 
   public NotebookRestApi() {}
 
-  public NotebookRestApi(Notebook notebook) {
+  public NotebookRestApi(Notebook notebook, NotebookServer notebookServer) {
+
     this.notebook = notebook;
+    this.notebookServer = notebookServer;
   }
 
   /**
@@ -109,4 +113,74 @@ public class NotebookRestApi {
     }
     return new JsonResponse(Status.OK, "", settingList).build();
   }
+
+  @GET
+  @Path("/")
+  public Response getNotebookList() throws IOException {
+    return new JsonResponse(Status.OK, "", notebook.getAllNotes() ).build();
+  }
+
+  /**
+   * Create new note REST API
+   * @param message - JSON with new note name
+   * @return JSON with new note ID
+   * @throws IOException
+   */
+  @POST
+  @Path("/")
+  public Response createNote(String message) throws IOException {
+    logger.info("Create new notebook by JSON {}" , message);
+    NewNotebookRequest request = gson.fromJson(message,
+        NewNotebookRequest.class);
+    Note note = notebook.createNote();
+    note.addParagraph(); // it's an empty note. so add one paragraph
+    String noteName = request.getName();
+    if (noteName.isEmpty()) {
+      noteName = "Note " + note.getId();
+    }
+    note.setName(noteName);
+    note.persist();
+    notebookServer.broadcastNote(note);
+    notebookServer.broadcastNoteList();
+    return new JsonResponse(Status.CREATED, "", note.getId() ).build();
+  }
+
+  /**
+   * Delete note REST API
+   * @param
+   * @return JSON with status.OK
+   * @throws IOException
+   */
+  @DELETE
+  @Path("{notebookId}")
+  public Response deleteNote(@PathParam("notebookId") String notebookId) throws IOException {
+    logger.info("Delete notebook {} ", notebookId);
+    if (!(notebookId.isEmpty())) {
+      Note note = notebook.getNote(notebookId);
+      if (note != null) {
+        notebook.removeNote(notebookId);
+      }
+    }
+    notebookServer.broadcastNoteList();
+    return new JsonResponse(Status.OK, "").build();
+  }
+  /**
+   * Clone note REST API
+   * @param
+   * @return JSON with status.CREATED
+   * @throws IOException
+   */
+  @POST
+  @Path("{notebookId}")
+  public Response cloneNote(@PathParam("notebookId") String notebookId, String message) throws
+      IOException, CloneNotSupportedException, IllegalArgumentException {
+    logger.info("clone notebook by JSON {}" , message);
+    NewNotebookRequest request = gson.fromJson(message,
+        NewNotebookRequest.class);
+    String newNoteName = request.getName();
+    Note newNote = notebook.cloneNote(notebookId, newNoteName);
+    notebookServer.broadcastNote(newNote);
+    notebookServer.broadcastNoteList();
+    return new JsonResponse(Status.CREATED, "", newNote.getId()).build();
+  }
 }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewNotebookRequest.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewNotebookRequest.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewNotebookRequest.java
new file mode 100644
index 0000000..8d1b8c0
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/message/NewNotebookRequest.java
@@ -0,0 +1,38 @@
+/*
+ * 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.rest.message;
+
+import java.util.Map;
+
+import org.apache.zeppelin.interpreter.InterpreterOption;
+
+/**
+ *  NewNotebookRequest rest api request message
+ *
+ */
+public class NewNotebookRequest {
+  String name;
+
+  public NewNotebookRequest (){
+
+  }
+
+  public String getName() {
+    return name;
+  }
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
index 4e48901..a6e944d 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/server/ZeppelinServer.java
@@ -271,7 +271,7 @@ public class ZeppelinServer extends Application {
     ZeppelinRestApi root = new ZeppelinRestApi();
     singletons.add(root);
 
-    NotebookRestApi notebookApi = new NotebookRestApi(notebook);
+    NotebookRestApi notebookApi = new NotebookRestApi(notebook, notebookServer);
     singletons.add(notebookApi);
 
     InterpreterRestApi interpreterApi = new InterpreterRestApi(replFactory);

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/zeppelin-server/src/main/java/org/apache/zeppelin/socket/NotebookServer.java
----------------------------------------------------------------------
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 2048cb9..c6aa846 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
@@ -273,11 +273,11 @@ public class NotebookServer extends WebSocketServlet implements
     }
   }
 
-  private void broadcastNote(Note note) {
+  public void broadcastNote(Note note) {
     broadcast(note.id(), new Message(OP.NOTE).put("note", note));
   }
 
-  private void broadcastNoteList() {
+  public void broadcastNoteList() {
     Notebook notebook = notebook();
 
     ZeppelinConfiguration conf = notebook.getConf();
@@ -430,22 +430,7 @@ public class NotebookServer extends WebSocketServlet implements
       throws IOException, CloneNotSupportedException {
     String noteId = getOpenNoteId(conn);
     String name = (String) fromMessage.get("name");
-    Note sourceNote = notebook.getNote(noteId);
-    Note newNote = notebook.createNote();
-    if (name != null) {
-      newNote.setName(name);
-    }
-    // Copy the interpreter bindings
-    List<String> boundInterpreterSettingsIds = notebook
-        .getBindedInterpreterSettingsIds(sourceNote.id());
-    notebook.bindInterpretersToNote(newNote.id(), boundInterpreterSettingsIds);
-
-    List<Paragraph> paragraphs = sourceNote.getParagraphs();
-    for (Paragraph para : paragraphs) {
-      Paragraph p = (Paragraph) para.clone();
-      newNote.addParagraph(p);
-    }
-    newNote.persist();
+    Note newNote = notebook.cloneNote(noteId, name);
     broadcastNote(newNote);
     broadcastNoteList();
   }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/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 e93815c..bffc888 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
@@ -17,10 +17,6 @@
 
 package org.apache.zeppelin.rest;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertThat;
-import static org.junit.Assert.assertTrue;
-
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
@@ -43,6 +39,9 @@ import org.junit.runners.MethodSorters;
 
 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
+
+import static org.junit.Assert.*;
+
 /**
  * BASIC Zeppelin rest api tests
  *
@@ -181,5 +180,107 @@ public class ZeppelinRestApiTest extends AbstractTestRestApi {
     }
     assertEquals("<p>markdown restarted</p>\n", p.getResult().message());
   }
+
+  @Test
+  public void testNotebookCreateWithName() throws IOException {
+    String noteName = "Test note name";
+    testNotebookCreate(noteName);
+  }
+
+  @Test
+  public void testNotebookCreateNoName() throws IOException {
+    testNotebookCreate("");
+  }
+
+  private void testNotebookCreate(String noteName) throws IOException {
+    // Call Create Notebook REST API
+    String jsonRequest = "{\"name\":\"" + noteName + "\"}";
+    PostMethod post = httpPost("/notebook/", jsonRequest);
+    LOG.info("testNotebookCreate \n" + post.getResponseBodyAsString());
+    assertThat("test notebook create method:", post, isCreated());
+
+    Map<String, Object> resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken<Map<String, Object>>() {
+    }.getType());
+
+    String newNotebookId =  (String) resp.get("body");
+    LOG.info("newNotebookId:=" + newNotebookId);
+    Note newNote = ZeppelinServer.notebook.getNote(newNotebookId);
+    assertNotNull("Can not find new note by id", newNote);
+    // This is partial test as newNote is in memory but is not persistent
+    String newNoteName = newNote.getName();
+    LOG.info("new note name is: " + newNoteName);
+    String expectedNoteName = noteName;
+    if (noteName.isEmpty()) {
+      expectedNoteName = "Note " + newNotebookId;
+    }
+    assertEquals("compare note name", expectedNoteName, newNoteName);
+    // cleanup
+    ZeppelinServer.notebook.removeNote(newNotebookId);
+    post.releaseConnection();
+
+  }
+
+  @Test
+  public void  testDeleteNote() throws IOException {
+    LOG.info("testDeleteNote");
+    //Create note and get ID
+    Note note = ZeppelinServer.notebook.createNote();
+    String noteId = note.getId();
+    testDeleteNotebook(noteId);
+  }
+
+  @Test
+  public void testDeleteNoteBadId() throws IOException {
+    LOG.info("testDeleteNoteBadId");
+    testDeleteNotebook("2AZFXEX97");
+    testDeleteNotebook("bad_ID");
+  }
+
+  private void testDeleteNotebook(String notebookId) throws IOException {
+
+    DeleteMethod delete = httpDelete(("/notebook/" + notebookId));
+    LOG.info("testDeleteNotebook delete response\n" + delete.getResponseBodyAsString());
+    assertThat("Test delete method:", delete, isAllowed());
+    delete.releaseConnection();
+    // make sure note is deleted
+    if (!notebookId.isEmpty()) {
+      Note deletedNote = ZeppelinServer.notebook.getNote(notebookId);
+      assertNull("Deleted note should be null", deletedNote);
+    }
+  }
+
+  @Test
+  public void testCloneNotebook() throws IOException, CloneNotSupportedException, IllegalArgumentException {
+    LOG.info("testCloneNotebook");
+    // Create note to clone
+    Note note = ZeppelinServer.notebook.createNote();
+    assertNotNull("cant create new note", note);
+    note.setName("source note for clone");
+    Paragraph paragraph = note.addParagraph();
+    paragraph.setText("%md This is my new paragraph in my new note");
+    note.persist();
+    String sourceNoteID = note.getId();
+
+    String noteName = "clone Note Name";
+    // Call Clone Notebook REST API
+    String jsonRequest = "{\"name\":\"" + noteName + "\"}";
+    PostMethod post = httpPost("/notebook/" + sourceNoteID, jsonRequest);
+    LOG.info("testNotebookClone \n" + post.getResponseBodyAsString());
+    assertThat("test notebook clone method:", post, isCreated());
+
+    Map<String, Object> resp = gson.fromJson(post.getResponseBodyAsString(), new TypeToken<Map<String, Object>>() {
+    }.getType());
+
+    String newNotebookId =  (String) resp.get("body");
+    LOG.info("newNotebookId:=" + newNotebookId);
+    Note newNote = ZeppelinServer.notebook.getNote(newNotebookId);
+    assertNotNull("Can not find new note by id", newNote);
+    assertEquals("Compare note names", noteName, newNote.getName());
+    assertEquals("Compare paragraphs count", note.getParagraphs().size(), newNote.getParagraphs().size());
+    //cleanup
+    ZeppelinServer.notebook.removeNote(note.getId());
+    ZeppelinServer.notebook.removeNote(newNote.getId());
+    post.releaseConnection();
+  }
 }
 

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/47df1cd1/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/Notebook.java
----------------------------------------------------------------------
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 63ff250..770172a 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
@@ -118,6 +118,37 @@ public class Notebook {
     return note;
   }
 
+  /**
+   * Clone existing note.
+   * @param sourceNoteID - the note ID to clone
+   * @param newNoteName - the name of the new note
+   * @return noteId
+   * @throws IOException, CloneNotSupportedException, IllegalArgumentException
+   */
+  public Note cloneNote(String sourceNoteID, String newNoteName) throws
+      IOException, CloneNotSupportedException, IllegalArgumentException {
+
+    Note sourceNote = getNote(sourceNoteID);
+    if (sourceNote == null) {
+      throw new IllegalArgumentException(sourceNoteID + "not found");
+    }
+    Note newNote = createNote();
+    if (newNoteName != null) {
+      newNote.setName(newNoteName);
+    }
+    // Copy the interpreter bindings
+    List<String> boundInterpreterSettingsIds = getBindedInterpreterSettingsIds(sourceNote.id());
+    bindInterpretersToNote(newNote.id(), boundInterpreterSettingsIds);
+
+    List<Paragraph> paragraphs = sourceNote.getParagraphs();
+    for (Paragraph para : paragraphs) {
+      Paragraph p = (Paragraph) para.clone();
+      newNote.addParagraph(p);
+    }
+    newNote.persist();
+    return newNote;
+  }
+
   public void bindInterpretersToNote(String id,
       List<String> interpreterSettingIds) throws IOException {
     Note note = getNote(id);