http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
deleted file mode 100644
index 7d3f001..0000000
--- 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/FolderView.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * 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.notebook;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * Folder view of notes of Notebook.
- * FolderView allows you to see notes from perspective of folders.
- */
-public class FolderView implements NoteNameListener, FolderListener {
-  // key: folderId
-  private final Map<String, Folder> folders = new LinkedHashMap<>();
-  // key: a note, value: a folder where the note belongs to
-  private final Map<Note, Folder> index = new LinkedHashMap<>();
-
-  private static final Logger logger = 
LoggerFactory.getLogger(FolderView.class);
-
-  public Folder getFolder(String folderId) {
-    String normalizedFolderId = Folder.normalizeFolderId(folderId);
-    return folders.get(normalizedFolderId);
-  }
-
-  /**
-   * Rename folder of which id is folderId to newFolderId
-   *
-   * @param oldFolderId folderId to rename
-   * @param newFolderId newFolderId
-   * @return `null` if folder not exists, else old Folder
-   * in order to know which notes and child folders are renamed
-   */
-  public Folder renameFolder(String oldFolderId, String newFolderId) {
-    String normOldFolderId = Folder.normalizeFolderId(oldFolderId);
-    String normNewFolderId = Folder.normalizeFolderId(newFolderId);
-
-    if (!hasFolder(normOldFolderId))
-      return null;
-
-    if (oldFolderId.equals(Folder.ROOT_FOLDER_ID))  // cannot rename the root 
folder
-      return null;
-
-    // check whether oldFolderId and newFolderId are same or not
-    if (normOldFolderId.equals(normNewFolderId))
-      return getFolder(normOldFolderId);
-
-    logger.info("Rename {} to {}", normOldFolderId, normNewFolderId);
-
-    Folder oldFolder = getFolder(normOldFolderId);
-    removeFolder(oldFolderId);
-
-    oldFolder.rename(normNewFolderId);
-
-    return oldFolder;
-  }
-
-  public Folder getFolderOf(Note note) {
-    return index.get(note);
-  }
-
-  public void putNote(Note note) {
-    if (note.isNameEmpty()) {
-      return;
-    }
-
-    String folderId = note.getFolderId();
-
-    Folder folder = getOrCreateFolder(folderId);
-    folder.addNote(note);
-
-    synchronized (index) {
-      index.put(note, folder);
-    }
-  }
-
-  private Folder getOrCreateFolder(String folderId) {
-    if (folders.containsKey(folderId))
-      return folders.get(folderId);
-
-    return createFolder(folderId);
-  }
-
-  private Folder createFolder(String folderId) {
-    folderId = Folder.normalizeFolderId(folderId);
-
-    Folder newFolder = new Folder(folderId);
-    newFolder.addFolderListener(this);
-
-    logger.info("Create folder {}", folderId);
-
-    synchronized (folders) {
-      folders.put(folderId, newFolder);
-    }
-
-    Folder parentFolder = getOrCreateFolder(newFolder.getParentFolderId());
-
-    newFolder.setParent(parentFolder);
-    parentFolder.addChild(newFolder);
-
-    return newFolder;
-  }
-
-  private void removeFolder(String folderId) {
-    Folder removedFolder;
-
-    synchronized (folders) {
-      removedFolder = folders.remove(folderId);
-    }
-
-    if (removedFolder != null) {
-      logger.info("Remove folder {}", folderId);
-      Folder parent = removedFolder.getParent();
-      parent.removeChild(folderId);
-      removeFolderIfEmpty(parent.getId());
-    }
-  }
-
-  private void removeFolderIfEmpty(String folderId) {
-    if (!hasFolder(folderId))
-      return;
-
-    Folder folder = getFolder(folderId);
-    if (folder.countNotes() == 0 && !folder.hasChild()) {
-      logger.info("Folder {} is empty", folder.getId());
-      removeFolder(folderId);
-    }
-  }
-
-  public void removeNote(Note note) {
-    if (!index.containsKey(note)) {
-      return;
-    }
-
-    Folder folder = index.get(note);
-    folder.removeNote(note);
-
-    removeFolderIfEmpty(folder.getId());
-
-    synchronized (index) {
-      index.remove(note);
-    }
-  }
-
-  public void clear() {
-    synchronized (folders) {
-      folders.clear();
-    }
-    synchronized (index) {
-      index.clear();
-    }
-  }
-
-  public boolean hasFolder(String folderId) {
-    return getFolder(folderId) != null;
-  }
-
-  public boolean hasNote(Note note) {
-    return index.containsKey(note);
-  }
-
-  public int countFolders() {
-    return folders.size();
-  }
-
-  public int countNotes() {
-    int count = 0;
-
-    for (Folder folder : folders.values()) {
-      count += folder.countNotes();
-    }
-
-    return count;
-  }
-
-  /**
-   * Fired after a note's setName() run.
-   * When the note's name changed, FolderView should check if the note is in 
the right folder.
-   *
-   * @param note
-   * @param oldName
-   */
-  @Override
-  public void onNoteNameChanged(Note note, String oldName) {
-    if (note.isNameEmpty()) {
-      return;
-    }
-    logger.info("Note name changed: {} -> {}", oldName, note.getName());
-    // New note
-    if (!index.containsKey(note)) {
-      putNote(note);
-    }
-    // Existing note
-    else {
-      // If the note is in the right place, just return
-      Folder folder = index.get(note);
-      if (folder.getId().equals(note.getFolderId())) {
-        return;
-      }
-      // The note's folder is changed!
-      removeNote(note);
-      putNote(note);
-    }
-  }
-
-  @Override
-  public void onFolderRenamed(Folder folder, String oldFolderId) {
-    if (getFolder(folder.getId()) == folder)  // the folder is at the right 
place
-      return;
-    logger.info("folder renamed: {} -> {}", oldFolderId, folder.getId());
-
-    if (getFolder(oldFolderId) == folder)
-      folders.remove(oldFolderId);
-
-    Folder newFolder = getOrCreateFolder(folder.getId());
-    newFolder.merge(folder);
-
-    for (Note note : folder.getNotes()) {
-      index.put(note, newFolder);
-    }
-  }
-}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/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 61a36ab..8f916e1 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
@@ -18,36 +18,30 @@
 package org.apache.zeppelin.notebook;
 
 import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Preconditions;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 import org.apache.commons.lang.StringUtils;
 import org.apache.zeppelin.common.JsonSerializable;
-import org.apache.zeppelin.completer.CompletionType;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
 import org.apache.zeppelin.display.AngularObject;
 import org.apache.zeppelin.display.AngularObjectRegistry;
 import org.apache.zeppelin.display.Input;
 import org.apache.zeppelin.interpreter.InterpreterFactory;
 import org.apache.zeppelin.interpreter.InterpreterGroup;
-import org.apache.zeppelin.interpreter.InterpreterInfo;
 import org.apache.zeppelin.interpreter.InterpreterResult;
-import org.apache.zeppelin.interpreter.InterpreterResultMessage;
 import org.apache.zeppelin.interpreter.InterpreterSetting;
 import org.apache.zeppelin.interpreter.InterpreterSettingManager;
 import org.apache.zeppelin.interpreter.remote.RemoteAngularObjectRegistry;
 import org.apache.zeppelin.interpreter.thrift.InterpreterCompletion;
-import org.apache.zeppelin.notebook.repo.NotebookRepo;
 import org.apache.zeppelin.notebook.utility.IdHashes;
-import org.apache.zeppelin.scheduler.Job;
 import org.apache.zeppelin.scheduler.Job.Status;
-import org.apache.zeppelin.search.SearchService;
 import org.apache.zeppelin.user.AuthenticationInfo;
 import org.apache.zeppelin.user.Credentials;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
 import java.util.Iterator;
@@ -55,18 +49,13 @@ import java.util.LinkedHashMap;
 import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.ScheduledThreadPoolExecutor;
-import java.util.concurrent.TimeUnit;
-
-import static java.lang.String.format;
 
 /**
- * Binded interpreters for a note
+ * Represent the note of Zeppelin. All the note and its paragraph operations 
are done
+ * via this class.
  */
 public class Note implements JsonSerializable {
   private static final Logger logger = LoggerFactory.getLogger(Note.class);
-  private static final long serialVersionUID = 7920699076577612429L;
   private static Gson gson = new GsonBuilder()
       .setPrettyPrinting()
       .setDateFormat("yyyy-MM-dd HH:mm:ss.SSS")
@@ -74,15 +63,7 @@ public class Note implements JsonSerializable {
       .registerTypeAdapterFactory(Input.TypeAdapterFactory)
       .create();
 
-  // threadpool for delayed persist of note
-  private static final ScheduledThreadPoolExecutor delayedPersistThreadPool =
-      new ScheduledThreadPoolExecutor(0);
-
-  static {
-    delayedPersistThreadPool.setRemoveOnCancelPolicy(true);
-  }
-
-  final List<Paragraph> paragraphs = new LinkedList<>();
+  private List<Paragraph> paragraphs = new LinkedList<>();
 
   private String name = "";
   private String id;
@@ -90,18 +71,6 @@ public class Note implements JsonSerializable {
   private Map<String, Object> noteParams = new LinkedHashMap<>();
   private Map<String, Input> noteForms = new LinkedHashMap<>();
   private Map<String, List<AngularObject>> angularObjects = new HashMap<>();
-
-  private transient InterpreterFactory factory;
-  private transient InterpreterSettingManager interpreterSettingManager;
-  private transient ParagraphJobListener paragraphJobListener;
-  private transient NotebookRepo repo;
-  private transient SearchService index;
-  private transient ScheduledFuture delayedPersist;
-  private transient Object delayedPersistLock = new Object();
-  private transient NoteEventListener noteEventListener;
-  private transient Credentials credentials;
-  private transient NoteNameListener noteNameListener;
-
   /*
    * note configurations.
    * - looknfeel - cron
@@ -115,29 +84,70 @@ public class Note implements JsonSerializable {
   private Map<String, Object> info = new HashMap<>();
 
 
+  /********************************** transient fields 
******************************************/
+  private transient boolean loaded = false;
+  private transient String path;
+  private transient InterpreterFactory interpreterFactory;
+  private transient InterpreterSettingManager interpreterSettingManager;
+  private transient ParagraphJobListener paragraphJobListener;
+  private transient List<NoteEventListener> noteEventListeners = new 
ArrayList<>();
+  private transient Credentials credentials;
+
+
   public Note() {
     generateId();
   }
 
-  public Note(String name, String defaultInterpreterGroup, NotebookRepo repo, 
InterpreterFactory factory,
+  public Note(String path, String defaultInterpreterGroup, InterpreterFactory 
factory,
       InterpreterSettingManager interpreterSettingManager, 
ParagraphJobListener paragraphJobListener,
-      SearchService noteIndex, Credentials credentials, NoteEventListener 
noteEventListener) {
-    this.name = name;
+      Credentials credentials, List<NoteEventListener> noteEventListener) {
+    setPath(path);
     this.defaultInterpreterGroup = defaultInterpreterGroup;
-    this.repo = repo;
-    this.factory = factory;
+    this.interpreterFactory = factory;
     this.interpreterSettingManager = interpreterSettingManager;
     this.paragraphJobListener = paragraphJobListener;
-    this.index = noteIndex;
-    this.noteEventListener = noteEventListener;
+    this.noteEventListeners = noteEventListener;
     this.credentials = credentials;
     generateId();
+
+    setCronSupported(ZeppelinConfiguration.create());
+  }
+
+  public Note(NoteInfo noteInfo) {
+    this.id = noteInfo.getId();
+    setPath(noteInfo.getPath());
+  }
+
+  public String getPath() {
+    return path;
+  }
+
+  public String getParentPath() {
+    int pos = path.lastIndexOf("/");
+    if (pos == 0) {
+      return "/";
+    } else {
+      return path.substring(0, pos);
+    }
+  }
+
+  private String getName(String path) {
+    int pos = path.lastIndexOf("/");
+    return path.substring(pos + 1);
   }
 
   private void generateId() {
     id = IdHashes.generateId();
   }
 
+  public boolean isLoaded() {
+    return loaded;
+  }
+
+  public void setLoaded(boolean loaded) {
+    this.loaded = loaded;
+  }
+
   public boolean isPersonalizedMode() {
     Object v = getConfig().get("personalizedMode");
     return null != v && "true".equals(v);
@@ -150,7 +160,7 @@ public class Note implements JsonSerializable {
     } else {
       valueString = "false";
     }
-    getConfig().put("personalizedMode", valueString);
+    config.put("personalizedMode", valueString);
     clearUserParagraphs(value);
   }
 
@@ -172,12 +182,18 @@ public class Note implements JsonSerializable {
   }
 
   public String getName() {
-    if (isNameEmpty()) {
-      name = getId();
-    }
     return name;
   }
 
+  public void setPath(String path) {
+    if (!path.startsWith("/")) {
+      this.path = "/" + path;
+    } else {
+      this.path = path;
+    }
+    this.name = getName(path);
+  }
+
   public String getDefaultInterpreterGroup() {
     return defaultInterpreterGroup;
   }
@@ -202,117 +218,36 @@ public class Note implements JsonSerializable {
     this.noteForms = noteForms;
   }
 
-  public String getNameWithoutPath() {
-    String notePath = getName();
-
-    int lastSlashIndex = notePath.lastIndexOf("/");
-    // The note is in the root folder
-    if (lastSlashIndex < 0) {
-      return notePath;
-    }
-
-    return notePath.substring(lastSlashIndex + 1);
-  }
-
-  /**
-   * @return normalized folder path, which is folderId
-   */
-  public String getFolderId() {
-    String notePath = getName();
-
-    // Ignore first '/'
-    if (notePath.charAt(0) == '/')
-      notePath = notePath.substring(1);
-
-    int lastSlashIndex = notePath.lastIndexOf("/");
-    // The root folder
-    if (lastSlashIndex < 0) {
-      return Folder.ROOT_FOLDER_ID;
-    }
-
-    String folderId = notePath.substring(0, lastSlashIndex);
-
-    return folderId;
-  }
-
-  public boolean isNameEmpty() {
-    return this.name.trim().isEmpty();
-  }
-
-  private String normalizeNoteName(String name) {
-    name = name.trim();
-    name = name.replace("\\", "/");
-    while (name.contains("///")) {
-      name = name.replaceAll("///", "/");
-    }
-    name = name.replaceAll("//", "/");
-    if (name.length() == 0) {
-      name = "/";
-    }
-    return name;
-  }
-
   public void setName(String name) {
-    String oldName = this.name;
-
-    if (name.indexOf('/') >= 0 || name.indexOf('\\') >= 0) {
-      name = normalizeNoteName(name);
-    }
     this.name = name;
-
-    if (this.noteNameListener != null && !oldName.equals(name)) {
-      noteNameListener.onNoteNameChanged(this, oldName);
+    if (this.path == null) {
+      if (name.startsWith("/")) {
+        this.path = name;
+      } else {
+        this.path = "/" + name;
+      }
+    } else {
+      int pos = this.path.indexOf("/");
+      this.path = this.path.substring(0, pos + 1) + this.name;
     }
   }
 
-  public void setNoteNameListener(NoteNameListener listener) {
-    this.noteNameListener = listener;
+  public InterpreterFactory getInterpreterFactory() {
+    return interpreterFactory;
   }
 
-  public void setInterpreterFactory(InterpreterFactory factory) {
-    this.factory = factory;
-    synchronized (paragraphs) {
-      for (Paragraph p : paragraphs) {
-        p.setInterpreterFactory(factory);
-      }
-    }
+  public void setInterpreterFactory(InterpreterFactory interpreterFactory) {
+    this.interpreterFactory = interpreterFactory;
   }
 
   void setInterpreterSettingManager(InterpreterSettingManager 
interpreterSettingManager) {
     this.interpreterSettingManager = interpreterSettingManager;
   }
 
-  public void initializeJobListenerForParagraph(Paragraph paragraph) {
-    final Note paragraphNote = paragraph.getNote();
-    if (!paragraphNote.getId().equals(this.getId())) {
-      throw new IllegalArgumentException(
-          format("The paragraph %s from note %s " + "does not belong to note 
%s", paragraph.getId(),
-              paragraphNote.getId(), this.getId()));
-    }
-
-    boolean foundParagraph = false;
-    for (Paragraph ownParagraph : paragraphs) {
-      if (paragraph.getId().equals(ownParagraph.getId())) {
-        paragraph.setListener(paragraphJobListener);
-        foundParagraph = true;
-      }
-    }
-
-    if (!foundParagraph) {
-      throw new IllegalArgumentException(
-          format("Cannot find paragraph %s " + "from note %s", 
paragraph.getId(),
-              paragraphNote.getId()));
-    }
-  }
-
   void setParagraphJobListener(ParagraphJobListener paragraphJobListener) {
     this.paragraphJobListener = paragraphJobListener;
   }
 
-  void setNotebookRepo(NotebookRepo repo) {
-    this.repo = repo;
-  }
-
   public Boolean isCronSupported(ZeppelinConfiguration config) {
     if (config.isZeppelinNotebookCronEnable()) {
       config.getZeppelinNotebookCronFolders();
@@ -334,10 +269,6 @@ public class Note implements JsonSerializable {
     getConfig().put("isZeppelinNotebookCronEnable", isCronSupported(config));
   }
 
-  public void setIndex(SearchService index) {
-    this.index = index;
-  }
-
   public Credentials getCredentials() {
     return credentials;
   }
@@ -346,7 +277,6 @@ public class Note implements JsonSerializable {
     this.credentials = credentials;
   }
 
-
   Map<String, List<AngularObject>> getAngularObjects() {
     return angularObjects;
   }
@@ -366,8 +296,7 @@ public class Note implements JsonSerializable {
   void addCloneParagraph(Paragraph srcParagraph, AuthenticationInfo subject) {
 
     // Keep paragraph original ID
-    final Paragraph newParagraph = new Paragraph(srcParagraph.getId(), this,
-        paragraphJobListener, factory);
+    Paragraph newParagraph = new Paragraph(srcParagraph.getId(), this, 
paragraphJobListener);
 
     Map<String, Object> config = new HashMap<>(srcParagraph.getConfig());
     Map<String, Object> param = srcParagraph.settings.getParams();
@@ -384,9 +313,7 @@ public class Note implements JsonSerializable {
     
     logger.debug("newParagraph user: " + newParagraph.getUser());
 
-
     try {
-      Gson gson = new Gson();
       String resultJson = gson.toJson(srcParagraph.getReturn());
       InterpreterResult result = InterpreterResult.fromJson(resultJson);
       newParagraph.setReturn(result, null);
@@ -399,8 +326,31 @@ public class Note implements JsonSerializable {
     synchronized (paragraphs) {
       paragraphs.add(newParagraph);
     }
-    if (noteEventListener != null) {
-      noteEventListener.onParagraphCreate(newParagraph);
+
+    try {
+      fireParagraphCreateEvent(newParagraph);
+    } catch (IOException e) {
+      e.printStackTrace();
+    }
+
+  }
+
+  public void fireParagraphCreateEvent(Paragraph p) throws IOException {
+    for (NoteEventListener listener : noteEventListeners) {
+      listener.onParagraphCreate(p);
+    }
+  }
+
+  public void fireParagraphRemoveEvent(Paragraph p) throws IOException {
+    for (NoteEventListener listener : noteEventListeners) {
+      listener.onParagraphRemove(p);
+    }
+  }
+
+
+  public void fireParagraphUpdateEvent(Paragraph p) throws IOException {
+    for (NoteEventListener listener : noteEventListeners) {
+      listener.onParagraphUpdate(p);
     }
   }
 
@@ -410,28 +360,25 @@ public class Note implements JsonSerializable {
    * @param index index of paragraphs
    */
   public Paragraph insertNewParagraph(int index, AuthenticationInfo 
authenticationInfo) {
-    Paragraph paragraph = createParagraph(index, authenticationInfo);
+    Paragraph paragraph = new Paragraph(this, paragraphJobListener);
+    paragraph.setAuthenticationInfo(authenticationInfo);
+    setParagraphMagic(paragraph, index);
     insertParagraph(paragraph, index);
     return paragraph;
   }
 
-  private Paragraph createParagraph(int index, AuthenticationInfo 
authenticationInfo) {
-    Paragraph p = new Paragraph(this, paragraphJobListener, factory);
-    p.setAuthenticationInfo(authenticationInfo);
-    setParagraphMagic(p, index);
-    return p;
-  }
-
   public void addParagraph(Paragraph paragraph) {
     insertParagraph(paragraph, paragraphs.size());
   }
 
-  public void insertParagraph(Paragraph paragraph, int index) {
+  private void insertParagraph(Paragraph paragraph, int index) {
     synchronized (paragraphs) {
       paragraphs.add(index, paragraph);
     }
-    if (noteEventListener != null) {
-      noteEventListener.onParagraphCreate(paragraph);
+    try {
+      fireParagraphCreateEvent(paragraph);
+    } catch (IOException e) {
+      e.printStackTrace();
     }
   }
 
@@ -449,11 +396,11 @@ public class Note implements JsonSerializable {
       while (i.hasNext()) {
         Paragraph p = i.next();
         if (p.getId().equals(paragraphId)) {
-          index.deleteIndexDoc(this, p);
           i.remove();
-
-          if (noteEventListener != null) {
-            noteEventListener.onParagraphRemove(p);
+          try {
+            fireParagraphRemoveEvent(p);
+          } catch (IOException e) {
+            e.printStackTrace();
           }
           return p;
         }
@@ -714,11 +661,7 @@ public class Note implements JsonSerializable {
   }
 
   public boolean isTrash() {
-    String path = getName();
-    if (path.charAt(0) == '/') {
-      path = path.substring(1);
-    }
-    return path.split("/")[0].equals(Folder.TRASH_FOLDER_ID);
+    return this.path.startsWith("/" + NoteManager.TRASH_FOLDER);
   }
 
   public List<InterpreterCompletion> completion(String paragraphId, String 
buffer, int cursor) {
@@ -792,26 +735,6 @@ public class Note implements JsonSerializable {
     }
   }
 
-  public void persist(AuthenticationInfo subject) throws IOException {
-    Preconditions.checkNotNull(subject, "AuthenticationInfo should not be 
null");
-    stopDelayedPersistTimer();
-    snapshotAngularObjectRegistry(subject.getUser());
-    index.updateIndexDoc(this);
-    repo.save(this, subject);
-  }
-
-  /**
-   * Persist this note with maximum delay.
-   */
-  public void persist(int maxDelaySec, AuthenticationInfo subject) {
-    startDelayedPersistTimer(maxDelaySec, subject);
-  }
-
-  void unpersist(AuthenticationInfo subject) throws IOException {
-    repo.remove(getId(), subject);
-  }
-
-
   /**
    * Return new note for specific user. this inserts and replaces user 
paragraph which doesn't
    * exists in original paragraph
@@ -838,35 +761,6 @@ public class Note implements JsonSerializable {
     return newNote;
   }
 
-  private void startDelayedPersistTimer(int maxDelaySec, final 
AuthenticationInfo subject) {
-    synchronized (delayedPersistLock) {
-      if (delayedPersist != null) {
-        return;
-      }
-
-      delayedPersist = delayedPersistThreadPool.schedule(new Runnable() {
-
-        @Override
-        public void run() {
-          try {
-            persist(subject);
-          } catch (IOException e) {
-            logger.error(e.getMessage(), e);
-          }
-        }
-      }, maxDelaySec, TimeUnit.SECONDS);
-    }
-  }
-
-  private void stopDelayedPersistTimer() {
-    synchronized (delayedPersistLock) {
-      if (delayedPersist == null) {
-        return;
-      }
-      delayedPersist.cancel(false);
-    }
-  }
-
   public Map<String, Object> getConfig() {
     if (config == null) {
       config = new HashMap<>();
@@ -889,8 +783,13 @@ public class Note implements JsonSerializable {
     this.info = info;
   }
 
-  void setNoteEventListener(NoteEventListener noteEventListener) {
-    this.noteEventListener = noteEventListener;
+  @Override
+  public String toString() {
+    if (this.path != null) {
+      return this.path;
+    } else {
+      return "/" + this.name;
+    }
   }
 
   @Override
@@ -944,9 +843,9 @@ public class Note implements JsonSerializable {
     if (paragraphs != null ? !paragraphs.equals(note.paragraphs) : 
note.paragraphs != null) {
       return false;
     }
-    //TODO(zjffdu) exclude name because FolderView.index use Note as key and 
consider different name
+    //TODO(zjffdu) exclude path because FolderView.index use Note as key and 
consider different path
     //as same note
-    //    if (name != null ? !name.equals(note.name) : note.name != null) 
return false;
+    //    if (path != null ? !path.equals(note.path) : note.path != null) 
return false;
     if (id != null ? !id.equals(note.id) : note.id != null) {
       return false;
     }
@@ -964,7 +863,7 @@ public class Note implements JsonSerializable {
   @Override
   public int hashCode() {
     int result = paragraphs != null ? paragraphs.hashCode() : 0;
-    //    result = 31 * result + (name != null ? name.hashCode() : 0);
+    //    result = 31 * result + (path != null ? path.hashCode() : 0);
     result = 31 * result + (id != null ? id.hashCode() : 0);
     result = 31 * result + (angularObjects != null ? angularObjects.hashCode() 
: 0);
     result = 31 * result + (config != null ? config.hashCode() : 0);
@@ -976,4 +875,8 @@ public class Note implements JsonSerializable {
   public static Gson getGson() {
     return gson;
   }
+
+  public void setNoteEventListeners(List<NoteEventListener> 
noteEventListeners) {
+    this.noteEventListeners = noteEventListeners;
+  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventAsyncListener.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventAsyncListener.java
 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventAsyncListener.java
new file mode 100644
index 0000000..b593673
--- /dev/null
+++ 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventAsyncListener.java
@@ -0,0 +1,228 @@
+/*
+ * 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.notebook;
+
+import org.apache.zeppelin.scheduler.Job;
+import org.apache.zeppelin.user.AuthenticationInfo;
+
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.LinkedBlockingQueue;
+
+/**
+ * An special NoteEventListener which handle events asynchronously
+ */
+public abstract class NoteEventAsyncListener implements NoteEventListener {
+
+  private BlockingQueue<NoteEvent> eventsQueue = new LinkedBlockingQueue<>();
+
+  private Thread eventHandlerThread;
+
+  public NoteEventAsyncListener(String name) {
+    this.eventHandlerThread = new EventHandlingThread();
+    this.eventHandlerThread.setName(name);
+    this.eventHandlerThread.start();
+  }
+
+  public abstract void handleNoteCreateEvent(NoteCreateEvent noteCreateEvent);
+
+  public abstract void handleNoteRemoveEvent(NoteRemoveEvent noteRemoveEvent);
+
+  public abstract void handleNoteUpdateEvent(NoteUpdateEvent noteUpdateEvent);
+
+  public abstract void handleParagraphCreateEvent(ParagraphCreateEvent 
paragraphCreateEvent);
+
+  public abstract void handleParagraphRemoveEvent(ParagraphRemoveEvent 
paragraphRemoveEvent);
+
+  public abstract void handleParagraphUpdateEvent(ParagraphUpdateEvent 
paragraphUpdateEvent);
+
+
+  public void close() {
+    this.eventHandlerThread.interrupt();
+  }
+
+  @Override
+  public void onNoteCreate(Note note, AuthenticationInfo subject) {
+    eventsQueue.add(new NoteCreateEvent(note, subject));
+  }
+
+  @Override
+  public void onNoteRemove(Note note, AuthenticationInfo subject) {
+    eventsQueue.add(new NoteRemoveEvent(note, subject));
+  }
+
+  @Override
+  public void onNoteUpdate(Note note, AuthenticationInfo subject) {
+    eventsQueue.add(new NoteUpdateEvent(note, subject));
+  }
+
+  @Override
+  public void onParagraphCreate(Paragraph p) {
+    eventsQueue.add(new ParagraphCreateEvent(p));
+  }
+
+  @Override
+  public void onParagraphRemove(Paragraph p) {
+    eventsQueue.add(new ParagraphRemoveEvent(p));
+  }
+
+  @Override
+  public void onParagraphUpdate(Paragraph p) {
+    eventsQueue.add(new ParagraphUpdateEvent(p));
+  }
+
+  @Override
+  public void onParagraphStatusChange(Paragraph p, Job.Status status) {
+    eventsQueue.add(new ParagraphStatusChangeEvent(p));
+  }
+
+  class EventHandlingThread extends Thread {
+
+    @Override
+    public void run() {
+      while(!Thread.interrupted()) {
+        try {
+          NoteEvent event = eventsQueue.take();
+          if (event instanceof NoteCreateEvent) {
+            handleNoteCreateEvent((NoteCreateEvent) event);
+          } else if (event instanceof NoteRemoveEvent) {
+            handleNoteRemoveEvent((NoteRemoveEvent) event);
+          } else if (event instanceof NoteUpdateEvent) {
+            handleNoteUpdateEvent((NoteUpdateEvent) event);
+          } else if (event instanceof ParagraphCreateEvent) {
+            handleParagraphCreateEvent((ParagraphCreateEvent) event);
+          } else if (event instanceof ParagraphRemoveEvent) {
+            handleParagraphRemoveEvent((ParagraphRemoveEvent) event);
+          } else if (event instanceof ParagraphUpdateEvent) {
+            handleParagraphUpdateEvent((ParagraphUpdateEvent) event);
+          } else {
+            throw new RuntimeException("Unknown event: " + 
event.getClass().getSimpleName());
+          }
+        } catch (InterruptedException e) {
+          e.printStackTrace();
+        }
+      }
+    }
+  }
+
+  /**
+   * Used for testing
+   *
+   * @throws InterruptedException
+   */
+  public void drainEvents() throws InterruptedException {
+    while(!eventsQueue.isEmpty()) {
+      Thread.sleep(1000);
+    }
+    Thread.sleep(5000);
+  }
+
+  interface NoteEvent {
+
+  }
+
+  public static class NoteCreateEvent implements NoteEvent {
+    private Note note;
+    private AuthenticationInfo subject;
+
+    public NoteCreateEvent(Note note, AuthenticationInfo subject) {
+      this.note = note;
+      this.subject = subject;
+    }
+
+    public Note getNote() {
+      return note;
+    }
+  }
+
+  public static class NoteUpdateEvent implements NoteEvent {
+    private Note note;
+    private AuthenticationInfo subject;
+
+    public NoteUpdateEvent(Note note, AuthenticationInfo subject) {
+      this.note = note;
+      this.subject = subject;
+    }
+
+    public Note getNote() {
+      return note;
+    }
+  }
+
+
+  public static class NoteRemoveEvent implements NoteEvent {
+    private Note note;
+    private AuthenticationInfo subject;
+
+    public NoteRemoveEvent(Note note, AuthenticationInfo subject) {
+      this.note = note;
+      this.subject = subject;
+    }
+
+    public Note getNote() {
+      return note;
+    }
+  }
+
+  public static class ParagraphCreateEvent implements NoteEvent {
+    private Paragraph p;
+
+    public ParagraphCreateEvent(Paragraph p) {
+      this.p = p;
+    }
+
+    public Paragraph getParagraph() {
+      return p;
+    }
+  }
+
+  public static class ParagraphUpdateEvent implements NoteEvent {
+    private Paragraph p;
+
+    public ParagraphUpdateEvent(Paragraph p) {
+      this.p = p;
+    }
+
+    public Paragraph getParagraph() {
+      return p;
+    }
+  }
+
+  public static class ParagraphRemoveEvent implements NoteEvent {
+    private Paragraph p;
+
+    public ParagraphRemoveEvent(Paragraph p) {
+      this.p = p;
+    }
+
+    public Paragraph getParagraph() {
+      return p;
+    }
+  }
+
+  public static class ParagraphStatusChangeEvent implements NoteEvent {
+    private Paragraph p;
+
+    public ParagraphStatusChangeEvent(Paragraph p) {
+      this.p = p;
+    }
+
+    public Paragraph getParagraph() {
+      return p;
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventListener.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventListener.java
 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventListener.java
index 5f98f70..442b4a6 100644
--- 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventListener.java
+++ 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteEventListener.java
@@ -17,12 +17,20 @@
 package org.apache.zeppelin.notebook;
 
 import org.apache.zeppelin.scheduler.Job;
+import org.apache.zeppelin.user.AuthenticationInfo;
+
+import java.io.IOException;
 
 /**
- * NoteEventListener
+ * Notebook event
  */
 public interface NoteEventListener {
-  public void onParagraphRemove(Paragraph p);
-  public void onParagraphCreate(Paragraph p);
-  public void onParagraphStatusChange(Paragraph p, Job.Status status);
+  void onNoteRemove(Note note, AuthenticationInfo subject) throws IOException;
+  void onNoteCreate(Note note, AuthenticationInfo subject) throws IOException;
+  void onNoteUpdate(Note note, AuthenticationInfo subject) throws IOException;
+
+  void onParagraphRemove(Paragraph p) throws IOException;
+  void onParagraphCreate(Paragraph p) throws IOException;
+  void onParagraphUpdate(Paragraph p) throws IOException;
+  void onParagraphStatusChange(Paragraph p, Job.Status status) throws 
IOException;
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java
index d316dfb..440b5fe 100644
--- a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java
+++ b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteInfo.java
@@ -17,28 +17,22 @@
 
 package org.apache.zeppelin.notebook;
 
-import java.util.HashMap;
-import java.util.Map;
-
 /**
- *
+ * Metadata of Note: noteId & note Path
  */
 public class NoteInfo {
   String id;
-  String name;
-  private Map<String, Object> config = new HashMap<>();
+  String path;
 
-  public NoteInfo(String id, String name, Map<String, Object> config) {
+  public NoteInfo(String id, String path) {
     super();
     this.id = id;
-    this.name = name;
-    this.config = config;
+    this.path = path;
   }
 
   public NoteInfo(Note note) {
     id = note.getId();
-    name = note.getName();
-    config = note.getConfig();
+    path = note.getPath();
   }
 
   public String getId() {
@@ -49,20 +43,26 @@ public class NoteInfo {
     this.id = id;
   }
 
-  public String getName() {
-    return name;
+  public String getPath() {
+    return path;
   }
 
-  public void setName(String name) {
-    this.name = name;
+  public void setPath(String path) {
+    this.path = path;
   }
 
-  public Map<String, Object> getConfig() {
-    return config;
+  public String getNoteName() {
+    int pos = this.path.lastIndexOf("/");
+    return path.substring(pos + 1);
   }
 
-  public void setConfig(Map<String, Object> config) {
-    this.config = config;
+  public String getParent() {
+    int pos = this.path.lastIndexOf("/");
+    return path.substring(0, pos);
   }
 
+  @Override
+  public String toString() {
+    return path + "_" + id + ".zpln";
+  }
 }

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteManager.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteManager.java 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteManager.java
new file mode 100644
index 0000000..edaabfe
--- /dev/null
+++ 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteManager.java
@@ -0,0 +1,569 @@
+/*
+ * 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.notebook;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.zeppelin.notebook.repo.NotebookRepo;
+import org.apache.zeppelin.scheduler.Job;
+import org.apache.zeppelin.user.AuthenticationInfo;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Manager class for note. It handle all the note related operations, such as 
get, create,
+ * delete & move note.
+ *
+ * It load 2 kinds of metadata into memory:
+ * 1. Mapping from noteId to note name
+ * 2. The tree structure of notebook folder
+ *
+ * Note will be loaded lazily. Initially only noteId nad note name is loaded,
+ * other note content is loaded until getNote is called.
+ *
+ * TODO(zjffdu) implement the lifecycle manager of Note
+ * (release memory if note is not used for some period)
+ */
+public class NoteManager {
+  private static final Logger LOGGER = 
LoggerFactory.getLogger(NoteManager.class);
+  public static String TRASH_FOLDER = "~Trash";
+  private Folder root;
+  private Folder trash;
+
+  private NotebookRepo notebookRepo;
+  private Map<String, String> notesInfo;
+
+  public NoteManager(NotebookRepo notebookRepo) throws IOException {
+    this.notebookRepo = notebookRepo;
+    this.root = new Folder("/", notebookRepo);
+    this.trash = this.root.getOrCreateFolder(TRASH_FOLDER);
+    init();
+  }
+
+  // build the tree structure of notes
+  private void init() throws IOException {
+    this.notesInfo = 
notebookRepo.list(AuthenticationInfo.ANONYMOUS).values().stream()
+        .collect(Collectors.toMap(noteInfo -> noteInfo.getId(), notesInfo -> 
notesInfo.getPath()));
+    this.notesInfo.entrySet().stream()
+        .forEach(entry ->
+        {
+          try {
+            addOrUpdateNoteNode(new Note(new NoteInfo(entry.getKey(), 
entry.getValue())));
+          } catch (IOException e) {
+            LOGGER.warn(e.getMessage());
+          }
+        });
+  }
+
+  public Map<String, String> getNotesInfo() {
+    return notesInfo;
+  }
+
+  //TODO(zjffdu) This is inefficient
+  public List<Note> getAllNotes() {
+    List<Note> notes = new ArrayList<>();
+    for (String notePath : notesInfo.values()) {
+      try {
+        notes.add(getNoteNode(notePath).getNote());
+      } catch (IOException e) {
+        LOGGER.warn("Fail to load note: " + notePath, e);
+      }
+    }
+    return notes;
+  }
+
+  /**
+   *
+   * @throws IOException
+   */
+  public void reloadNotes() throws IOException {
+    this.root = new Folder("/", notebookRepo);
+    this.trash = this.root.getOrCreateFolder(TRASH_FOLDER);
+    init();
+  }
+
+  private void addOrUpdateNoteNode(Note note, boolean checkDuplicates) throws 
IOException {
+    String notePath = note.getPath();
+    String[] tokens = notePath.split("/");
+    Folder curFolder = root;
+    for (int i = 0; i < tokens.length - 1; ++i) {
+      if (!StringUtils.isBlank(tokens[i])) {
+        curFolder = curFolder.getOrCreateFolder(tokens[i]);
+      }
+    }
+    if (checkDuplicates && curFolder.containsNote(tokens[tokens.length - 1])) {
+      throw new IOException("Note " + note.getPath() + " existed");
+    }
+    curFolder.addNote(tokens[tokens.length -1], note);
+    this.notesInfo.put(note.getId(), note.getPath());
+  }
+
+  private void addOrUpdateNoteNode(Note note) throws IOException {
+    addOrUpdateNoteNode(note, false);
+  }
+
+  /**
+   * Check whether there exist note under this notePath.
+   *
+   * @param notePath
+   * @return
+   */
+  public boolean containsNote(String notePath) {
+    try {
+      getNoteNode(notePath);
+      return true;
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  /**
+   * Check whether there exist such folder.
+   *
+   * @param folderPath
+   * @return
+   */
+  public boolean containsFolder(String folderPath) {
+    try {
+      getFolder(folderPath);
+      return true;
+    } catch (IOException e) {
+      return false;
+    }
+  }
+
+  /**
+   * Save note to NoteManager, it won't check duplicates, this is used when 
updating note.
+   *
+   * @param note
+   * @param subject
+   * @throws IOException
+   */
+  public void saveNote(Note note, AuthenticationInfo subject) throws 
IOException {
+    addOrUpdateNoteNode(note);
+    this.notebookRepo.save(note, subject);
+    note.setLoaded(true);
+  }
+
+  /**
+   * Add or update Note
+   *
+   * @param note
+   * @throws IOException
+   */
+  public void saveNote(Note note) throws IOException {
+    saveNote(note, AuthenticationInfo.ANONYMOUS);
+  }
+
+  /**
+   * Remove note from NotebookRepo and NoteManager
+   *
+   * @param noteId
+   * @param subject
+   * @throws IOException
+   */
+  public void removeNote(String noteId, AuthenticationInfo subject) throws 
IOException {
+    String notePath = this.notesInfo.remove(noteId);
+    Folder folder = getOrCreateFolder(getFolderName(notePath));
+    folder.removeNote(getNoteName(notePath));
+    this.notebookRepo.remove(noteId, notePath, subject);
+  }
+
+  public void moveNote(String noteId,
+                       String newNotePath,
+                       AuthenticationInfo subject) throws IOException {
+    String notePath = this.notesInfo.get(noteId);
+    if (noteId == null) {
+      throw new IOException("No metadata found for this note: " + noteId);
+    }
+
+    // move the old NoteNode from notePath to newNotePath
+    NoteNode noteNode = getNoteNode(notePath);
+    noteNode.getParent().removeNote(getNoteName(notePath));
+    noteNode.setNotePath(newNotePath);
+    String newParent = getFolderName(newNotePath);
+    Folder newFolder = getOrCreateFolder(newParent);
+    newFolder.addNoteNode(noteNode);
+
+    // update noteInfo mapping
+    this.notesInfo.put(noteId, newNotePath);
+
+    // update notebookrepo
+    this.notebookRepo.move(noteId, notePath, newNotePath, subject);
+  }
+
+
+  public void moveFolder(String folderPath,
+                         String newFolderPath,
+                         AuthenticationInfo subject) throws IOException {
+
+    // update notebookrepo
+    this.notebookRepo.move(folderPath, newFolderPath, subject);
+
+    // update filesystem tree
+    Folder folder = getFolder(folderPath);
+    folder.getParent().removeFolder(folder.getName(), subject);
+    Folder newFolder = getOrCreateFolder(newFolderPath);
+    newFolder.getParent().addFolder(newFolder.getName(), folder);
+
+    // update notesInfo
+    for (Note note : folder.getRawNotesRecursively()) {
+      notesInfo.put(note.getId(), note.getPath());
+    }
+  }
+
+  /**
+   * Remove the folder from the tree and returns the affected NoteInfo under 
this folder.
+   *
+   * @param folderPath
+   * @param subject
+   * @return
+   * @throws IOException
+   */
+  public List<Note> removeFolder(String folderPath, AuthenticationInfo 
subject) throws IOException {
+
+    // update notebookrepo
+    this.notebookRepo.remove(folderPath, subject);
+
+    // update filesystem tree
+    Folder folder = getFolder(folderPath);
+    List<Note> notes = folder.getParent().removeFolder(folder.getName(), 
subject);
+
+    // update notesInfo
+    for (Note note : notes) {
+      this.notesInfo.remove(note.getId());
+    }
+
+    return notes;
+  }
+
+  public Note getNote(String noteId) throws IOException {
+    String notePath = this.notesInfo.get(noteId);
+    if (notePath == null) {
+      return null;
+    }
+    NoteNode noteNode = getNoteNode(notePath);
+    return noteNode.getNote();
+  }
+
+  /**
+   *
+   * @param folderName  Absolute path of folder name
+   * @return
+   */
+  public Folder getOrCreateFolder(String folderName) {
+    String[] tokens = folderName.split("/");
+    Folder curFolder = root;
+    for (int i = 0; i < tokens.length; ++i) {
+      if (!StringUtils.isBlank(tokens[i])) {
+        curFolder = curFolder.getOrCreateFolder(tokens[i]);
+      }
+    }
+    return curFolder;
+  }
+
+  private NoteNode getNoteNode(String notePath) throws IOException {
+    String[] tokens = notePath.split("/");
+    Folder curFolder = root;
+    for (int i = 0; i < tokens.length - 1; ++i) {
+      if (!StringUtils.isBlank(tokens[i])) {
+        curFolder = curFolder.getFolder(tokens[i]);
+        if (curFolder == null) {
+          throw new IOException("Can not find note: " + notePath);
+        }
+      }
+    }
+    NoteNode noteNode = curFolder.getNote(tokens[tokens.length - 1]);
+    if (noteNode == null) {
+      throw new IOException("Can not find note: " + notePath);
+    }
+    return noteNode;
+  }
+
+  private Folder getFolder(String folderPath) throws IOException {
+    String[] tokens = folderPath.split("/");
+    Folder curFolder = root;
+    for (int i = 0; i < tokens.length; ++i) {
+      if (!StringUtils.isBlank(tokens[i])) {
+        curFolder = curFolder.getFolder(tokens[i]);
+        if (curFolder == null) {
+          throw new IOException("Can not find folder: " + folderPath);
+        }
+      }
+    }
+    return curFolder;
+  }
+
+  public Folder getTrashFolder() {
+    return this.trash;
+  }
+
+  private String getFolderName(String notePath) {
+    int pos = notePath.lastIndexOf("/");
+    return notePath.substring(0, pos);
+  }
+
+  private String getNoteName(String notePath) {
+    int pos = notePath.lastIndexOf("/");
+    return notePath.substring(pos + 1);
+  }
+
+  /**
+   * Represent one folder that could contains sub folders and note files.
+   */
+  public static class Folder {
+
+    private String name;
+    private Folder parent;
+    private NotebookRepo notebookRepo;
+
+    // noteName -> NoteNode
+    private Map<String, NoteNode> notes = new HashMap<>();
+    // folderName -> Folder
+    private Map<String, Folder> subFolders = new HashMap<>();
+
+    public Folder(String name, NotebookRepo notebookRepo) {
+      this.name = name;
+      this.notebookRepo = notebookRepo;
+    }
+
+    public Folder(String name, Folder parent, NotebookRepo notebookRepo) {
+      this(name, notebookRepo);
+      this.parent = parent;
+    }
+
+    public synchronized Folder getOrCreateFolder(String folderName) {
+      if (StringUtils.isBlank(folderName)) {
+        return this;
+      }
+      if (!subFolders.containsKey(folderName)) {
+        subFolders.put(folderName, new Folder(folderName, this, notebookRepo));
+      }
+      return subFolders.get(folderName);
+    }
+
+    public Folder getParent() {
+      return parent;
+    }
+
+    public void setParent(Folder parent) {
+      this.parent = parent;
+    }
+
+    public String getName() {
+      return name;
+    }
+
+    public void setName(String name) {
+      this.name = name;
+    }
+
+    public Folder getFolder(String folderName) {
+      return subFolders.get(folderName);
+    }
+
+    public Map<String, Folder> getFolders() {
+      return subFolders;
+    }
+
+    public NoteNode getNote(String noteName) {
+      return this.notes.get(noteName);
+    }
+
+    public void addNote(String noteName, Note note) {
+      notes.put(noteName, new NoteNode(note, this, notebookRepo));
+    }
+
+    /**
+     * Attach another folder under this folder, this is used when moving 
folder.
+     * The path of notes under this folder also need to be updated.
+     */
+    public void addFolder(String folderName, Folder folder) throws IOException 
{
+      subFolders.put(folderName, folder);
+      folder.setParent(this);
+      folder.setName(folderName);
+      for (NoteNode noteNode : folder.getNoteNodeRecursively()) {
+        noteNode.updateNotePath();
+      }
+    }
+
+    public boolean containsNote(String noteName) {
+      return notes.containsKey(noteName);
+    }
+
+    /**
+     * Attach note under this folder, this is used when moving note
+     * @param noteNode
+     */
+    public void addNoteNode(NoteNode noteNode) {
+      this.notes.put(noteNode.getNoteName(), noteNode);
+      noteNode.setParent(this);
+    }
+
+    public void removeNote(String noteName) {
+      this.notes.remove(noteName);
+    }
+
+    public List<Note> removeFolder(String folderName,
+                                   AuthenticationInfo subject) throws 
IOException {
+      Folder folder = this.subFolders.remove(folderName);
+      return folder.getRawNotesRecursively();
+    }
+
+    public List<Note> getRawNotesRecursively() {
+      List<Note> notesInfo = new ArrayList<>();
+      for (NoteNode noteNode : this.notes.values()) {
+        notesInfo.add(noteNode.getRawNote());
+      }
+      for (Folder folder : subFolders.values()) {
+        notesInfo.addAll(folder.getRawNotesRecursively());
+      }
+      return notesInfo;
+    }
+
+    public List<NoteNode> getNoteNodeRecursively() {
+      List<NoteNode> notes = new ArrayList<>();
+      notes.addAll(this.notes.values());
+      for (Folder folder : subFolders.values()) {
+        notes.addAll(folder.getNoteNodeRecursively());
+      }
+      return notes;
+    }
+
+    public Map<String, NoteNode> getNotes() {
+      return notes;
+    }
+
+    public String getPath() {
+      // root
+      if (name.equals("/")) {
+        return name;
+      }
+      // folder under root
+      if (parent.name.equals("/")) {
+        return "/" + name;
+      }
+      // other cases
+      return parent.toString() + "/" + name;
+    }
+
+    @Override
+    public String toString() {
+      return getPath();
+    }
+  }
+
+  /**
+   * One node in the file system tree structure which represent the note.
+   * This class has 2 usage scenarios:
+   * 1. metadata of note (only noteId and note name is loaded via reading the 
file name)
+   * 2. the note object (note content is loaded from NotebookRepo)
+   *
+   * It will load note from NotebookRepo lazily until method getNote is called.
+   */
+  public static class NoteNode {
+
+    private Folder parent;
+    private Note note;
+    private NotebookRepo notebookRepo;
+
+    public NoteNode(Note note, Folder parent, NotebookRepo notebookRepo) {
+      this.note = note;
+      this.parent = parent;
+      this.notebookRepo = notebookRepo;
+    }
+
+    /**
+     * This method will load note from NotebookRepo. If you just want to get 
noteId, noteName or
+     * notePath, you can call method getNoteId, getNoteName & getNotePath
+     * @return
+     * @throws IOException
+     */
+    public synchronized Note getNote() throws IOException {
+      if (!note.isLoaded()) {
+        note = notebookRepo.get(note.getId(), note.getPath(), 
AuthenticationInfo.ANONYMOUS);
+        if (parent.toString().equals("/")) {
+          note.setPath("/" + note.getName());
+        } else {
+          note.setPath(parent.toString() + "/" + note.getName());
+        }
+        note.setLoaded(true);
+      }
+      return note;
+    }
+
+    public String getNoteId() {
+      return this.note.getId();
+    }
+
+    public String getNoteName() {
+      return this.note.getName();
+    }
+
+    public String getNotePath() {
+      if (parent.getPath().equals("/")) {
+        return parent.getPath() + note.getName();
+      } else {
+        return parent.getPath() + "/" + note.getName();
+      }
+    }
+
+    /**
+     * This method will just return the note object without checking whether 
it is loaded
+     * from NotebookRepo.
+     *
+     * @return
+     */
+    public Note getRawNote() {
+      return this.note;
+    }
+
+    public Folder getParent() {
+      return parent;
+    }
+
+    @Override
+    public String toString() {
+      return getNotePath();
+    }
+
+    public void setParent(Folder parent) {
+      this.parent = parent;
+    }
+
+    public void setNotePath(String notePath) {
+      this.note.setPath(notePath);
+    }
+
+    /**
+     * This is called when the ancestor folder is moved.
+     */
+    public void updateNotePath() {
+      this.note.setPath(getNotePath());
+    }
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/zeppelin/blob/085efeb6/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteNameListener.java
----------------------------------------------------------------------
diff --git 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteNameListener.java
 
b/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteNameListener.java
deleted file mode 100644
index 28b53fb..0000000
--- 
a/zeppelin-zengine/src/main/java/org/apache/zeppelin/notebook/NoteNameListener.java
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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.notebook;
-
-/**
- * NoteNameListener. It's used by FolderView.
- */
-public interface NoteNameListener {
-  /**
-   * Fired after note name changed
-   * @param note
-   * @param oldName
-   */
-  void onNoteNameChanged(Note note, String oldName);
-}

Reply via email to