/*
* 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.*;
/**
* Represents a folder of Notebook. ID of the folder is a normalized path of it.
* 'normalized path' means the path that removed '/' from the beginning and the end of the path.
* e.g. "a/b/c", but not "/a/b/c", "a/b/c/" or "/a/b/c/".
* One exception can be the root folder, which is '/'.
*/
public class Folder {
public static final String ROOT_FOLDER_ID = "/";
public static final String TRASH_FOLDER_ID = "~Trash";
public static final String TRASH_FOLDER_CONFLICT_INFIX = " ";
private String id;
private Folder parent;
private Map<String, Folder> children = new LinkedHashMap<>();
// key: noteId
private final Map<String, Note> notes = new LinkedHashMap<>();
private List<FolderListener> listeners = new LinkedList<>();
private static final Logger logger = LoggerFactory.getLogger(Folder.class);
public Folder(String id) {
this.id = id;
}
public String getId() {
return id;
}
public String getName() {
if (isRoot())
return ROOT_FOLDER_ID;
String path = getId();
int lastSlashIndex = path.lastIndexOf("/");
if (lastSlashIndex < 0) { // This folder is under the root
return path;
}
return path.substring(lastSlashIndex + 1);
}
public String getParentFolderId() {
if (isRoot())
return ROOT_FOLDER_ID;
int lastSlashIndex = getId().lastIndexOf("/");
// The root folder
if (lastSlashIndex < 0) {
return Folder.ROOT_FOLDER_ID;
}
return getId().substring(0, lastSlashIndex);
}
public static String normalizeFolderId(String id) {
id = id.trim();
id = id.replace("\\", "/");
while (id.contains("///")) {
id = id.replaceAll("///", "/");
}
id = id.replaceAll("//", "/");
if (id.equals(ROOT_FOLDER_ID)) {
return ROOT_FOLDER_ID;
}
if (id.charAt(0) == '/') {
id = id.substring(1);
}
if (id.charAt(id.length() - 1) == '/') {
id = id.substring(0, id.length() - 1);
}
return id;
}
/**
* Rename this folder as well as the notes and the children belong to it
*
* @param newId
*/
public void rename(String newId) {
if (isRoot()) // root folder cannot be renamed
return;
String oldId = getId();
id = normalizeFolderId(newId);
logger.info("Rename {} to {}", oldId, getId());
synchronized (notes) {
for (Note note : notes.values()) {
String noteName = note.getNameWithoutPath();
String newNotePath;
if (newId.equals(ROOT_FOLDER_ID)) {
newNotePath = noteName;
} else {
newNotePath = newId + "/" + noteName;
}
note.setName(newNotePath);
}
}
for (Folder child : children.values()) {
child.rename(getId() + "/" + child.getName());
}
notifyRenamed(oldId);
}
/**
* Merge folder's notes and child folders
*
* @param folder
*/
public void merge(Folder folder) {
logger.info("Merge {} into {}", folder.getId(), getId());
addNotes(folder.getNotes());
}
public void addFolderListener(FolderListener listener) {
listeners.add(listener);
}
public void notifyRenamed(String oldFolderId) {
for (FolderListener listener : listeners) {
listener.onFolderRenamed(this, oldFolderId);
}
}
public Folder getParent() {
return parent;
}
public Map<String, Folder> getChildren() {
return children;
}
public void setParent(Folder parent) {
logger.info("Set parent of {} to {}", getId(), parent.getId());
this.parent = parent;
}
public void addChild(Folder child) {
if (child == this) // prevent the root folder from setting itself as child
return;
children.put(child.getId(), child);
}
public void removeChild(String folderId) {
logger.info("Remove child {} from {}", folderId, getId());
children.remove(folderId);
}
public void addNote(Note note) {
logger.info("Add note {} to folder {}", note.getId(), getId());
synchronized (notes) {
notes.put(note.getId(), note);
}
}
public void addNotes(List<Note> newNotes) {
synchronized (notes) {
for (Note note : newNotes) {
notes.put(note.getId(), note);
}
}
}
public void removeNote(Note note) {
logger.info("Remove note {} from folder {}", note.getId(), getId());
synchronized (notes) {
notes.remove(note.getId());
}
}
public List<Note> getNotes() {
return new LinkedList<>(notes.values());
}
public List<Note> getNotesRecursively() {
List<Note> notes = getNotes();
for (Folder child : children.values()) {
notes.addAll(child.getNotesRecursively());
}
return notes;
}
public int countNotes() {
return notes.size();
}
public boolean hasChild() {
return children.size() > 0;
}
boolean isRoot() {
return getId().equals(ROOT_FOLDER_ID);
}
public boolean isTrash() {
if (isRoot())
return false;
return getId().split("/")[0].equals(TRASH_FOLDER_ID);
}
}