/* This code is part of Freenet. It is distributed under the GNU General
* Public License, version 2 (or at your option any later version). See
* http://www.gnu.org/ for further details of the GPL. */
package freenet.clients.http.bookmark;
import static java.util.concurrent.TimeUnit.MINUTES;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import freenet.client.async.ClientContext;
import freenet.client.async.USKCallback;
import freenet.clients.http.FProxyToadlet;
import freenet.keys.FreenetURI;
import freenet.keys.USK;
import freenet.l10n.NodeL10n;
import freenet.node.FSParseException;
import freenet.node.NodeClientCore;
import freenet.node.RequestClient;
import freenet.node.RequestStarter;
import freenet.support.LogThresholdCallback;
import freenet.support.Logger;
import freenet.support.Logger.LogLevel;
import freenet.support.SimpleFieldSet;
import freenet.support.io.Closer;
import freenet.support.io.FileUtil;
public class BookmarkManager implements RequestClient {
public static final SimpleFieldSet DEFAULT_BOOKMARKS;
private final NodeClientCore node;
private final USKUpdatedCallback uskCB = new USKUpdatedCallback();
public static final BookmarkCategory MAIN_CATEGORY = new BookmarkCategory("/");
public static final BookmarkCategory DEFAULT_CATEGORY = new BookmarkCategory("\\");
private final HashMap<String, Bookmark> bookmarks = new HashMap<String, Bookmark>();
private final File bookmarksFile;
private final File backupBookmarksFile;
private boolean isSavingBookmarks = false;
private boolean isSavingBookmarksLazy = false;
static {
String name = "freenet/clients/http/staticfiles/defaultbookmarks.dat";
SimpleFieldSet defaultBookmarks = null;
InputStream in = null;
try {
ClassLoader loader = BookmarkManager.class.getClassLoader();
// Returns null on lookup failures:
in = loader.getResourceAsStream(name);
if(in != null)
defaultBookmarks = SimpleFieldSet.readFrom(in, false, false);
} catch(Exception e) {
Logger.error(BookmarkManager.class, "Error while loading the default bookmark file from " + name + " :" + e.getMessage(), e);
} finally {
Closer.close(in);
DEFAULT_BOOKMARKS = defaultBookmarks;
}
}
private static volatile boolean logMINOR;
static {
Logger.registerLogThresholdCallback(new LogThresholdCallback(){
@Override
public void shouldUpdate(){
logMINOR = Logger.shouldLog(LogLevel.MINOR, this);
}
});
}
public BookmarkManager(NodeClientCore n, boolean publicGateway) {
putPaths("/", MAIN_CATEGORY);
this.node = n;
this.bookmarksFile = n.node.userDir().file("bookmarks.dat");
this.backupBookmarksFile = n.node.userDir().file("bookmarks.dat.bak");
try {
// Read the backup file if necessary
if(!bookmarksFile.exists() || bookmarksFile.length() == 0)
throw new IOException();
Logger.normal(this, "Attempting to read the bookmark file from " + bookmarksFile.toString());
SimpleFieldSet sfs = SimpleFieldSet.readFrom(bookmarksFile, false, true);
readBookmarks(MAIN_CATEGORY, sfs);
} catch(MalformedURLException mue) {
} catch(IOException ioe) {
Logger.error(this, "Error reading the bookmark file (" + bookmarksFile.toString() + "):" + ioe.getMessage(), ioe);
try {
if(backupBookmarksFile.exists() && backupBookmarksFile.canRead() && backupBookmarksFile.length() > 0) {
Logger.normal(this, "Attempting to read the backup bookmark file from " + backupBookmarksFile.toString());
SimpleFieldSet sfs = SimpleFieldSet.readFrom(backupBookmarksFile, false, true);
readBookmarks(MAIN_CATEGORY, sfs);
} else {
Logger.normal(this, "We couldn't find the backup either! - " + FileUtil.getCanonicalFile(backupBookmarksFile));
// restore the default bookmark set
readBookmarks(MAIN_CATEGORY, DEFAULT_BOOKMARKS);
}
} catch(IOException e) {
Logger.error(this, "Error reading the backup bookmark file !" + e.getMessage(), e);
}
}
//populate defaults for hosts without full access permissions if we're in gateway mode.
if (publicGateway) {
putPaths("\\", DEFAULT_CATEGORY);
readBookmarks(DEFAULT_CATEGORY, DEFAULT_BOOKMARKS);
}
}
public void reAddDefaultBookmarks() {
BookmarkCategory bc = new BookmarkCategory(l10n("defaultBookmarks") + " - " + new Date());
addBookmark("/", bc);
_innerReadBookmarks("/", bc, DEFAULT_BOOKMARKS);
}
private class USKUpdatedCallback implements USKCallback {
@Override
public void onFoundEdition(long edition, USK key, ClientContext context, boolean wasMetadata, short codec, byte[] data, boolean newKnownGood, boolean newSlotToo) {
if(!newKnownGood) {
FreenetURI uri = key.copy(edition).getURI();
node.makeClient(PRIORITY_PROGRESS, false, false).prefetch(uri, MINUTES.toMillis(60), FProxyToadlet.MAX_LENGTH_WITH_PROGRESS, null, PRIORITY_PROGRESS);
return;
}
List<BookmarkItem> items = MAIN_CATEGORY.getAllItems();
boolean matched = false;
boolean updated = false;
for(int i = 0; i < items.size(); i++) {
if(!"USK".equals(items.get(i).getKeyType()))
continue;
try {
FreenetURI furi = new FreenetURI(items.get(i).getKey());
USK usk = USK.create(furi);
if(usk.equals(key, false)) {
if(logMINOR) Logger.minor(this, "Updating bookmark for "+furi+" to edition "+edition);
matched = true;
BookmarkItem item = items.get(i);
updated |= item.setEdition(edition, node);
// We may have bookmarked the same site twice, so continue the search.
}
} catch(MalformedURLException mue) {
}
}
if(updated) {
storeBookmarksLazy();
} else if(!matched) {
Logger.error(this, "No match for bookmark "+key+" edition "+edition);
}
}
@Override
public short getPollingPriorityNormal() {
return PRIORITY;
}
@Override
public short getPollingPriorityProgress() {
return PRIORITY_PROGRESS;
}
}
public String l10n(String key) {
return NodeL10n.getBase().getString("BookmarkManager." + key);
}
public String parentPath(String path) {
if(path.equals("/"))
return "/";
return path.substring(0, path.substring(0, path.length() - 1).lastIndexOf('/')) + "/";
}
public Bookmark getBookmarkByPath(String path) {
synchronized(bookmarks) {
return bookmarks.get(path);
}
}
public BookmarkCategory getCategoryByPath(String path) {
Bookmark cat = getBookmarkByPath(path);
if(cat instanceof BookmarkCategory)
return (BookmarkCategory) cat;
return null;
}
public BookmarkItem getItemByPath(String path) {
if(getBookmarkByPath(path) instanceof BookmarkItem)
return (BookmarkItem) getBookmarkByPath(path);
return null;
}
public void addBookmark(String parentPath, Bookmark bookmark) {
if(logMINOR)
Logger.minor(this, "Adding bookmark " + bookmark + " to " + parentPath);
BookmarkCategory parent = getCategoryByPath(parentPath);
parent.addBookmark(bookmark);
putPaths(parentPath + bookmark.getName() + ((bookmark instanceof BookmarkCategory) ? "/" : ""),
bookmark);
if(bookmark instanceof BookmarkItem)
subscribeToUSK((BookmarkItem)bookmark);
}
public void renameBookmark(String path, String newName) {
Bookmark bookmark = getBookmarkByPath(path);
String oldName = bookmark.getName();
String oldPath = '/' + oldName;
String newPath = path.substring(0, path.indexOf(oldPath)) + '/' + newName + (bookmark instanceof BookmarkCategory ? "/" : "");
bookmark.setName(newName);
synchronized(bookmarks) {
Iterator<String> it = bookmarks.keySet().iterator();
while(it.hasNext()) {
String s = it.next();
if(s.startsWith(path)) {
it.remove();
}
}
putPaths(newPath, bookmark);
}
storeBookmarks();
}
public void moveBookmark(String bookmarkPath, String newParentPath) {
Bookmark b = getBookmarkByPath(bookmarkPath);
addBookmark(newParentPath, b);
getCategoryByPath(parentPath(bookmarkPath)).removeBookmark(b);
removePaths(bookmarkPath);
}
public void removeBookmark(String path) {
Bookmark bookmark = getBookmarkByPath(path);
if(bookmark == null)
return;
if(bookmark instanceof BookmarkCategory) {
BookmarkCategory cat = (BookmarkCategory) bookmark;
for(int i = 0; i < cat.size(); i++)
removeBookmark(path + cat.get(i).getName() + ((cat.get(i) instanceof BookmarkCategory) ? "/"
: ""));
} else {
if(((BookmarkItem) bookmark).getKeyType().equals("USK")) {
try {
USK u = ((BookmarkItem) bookmark).getUSK();
if(!wantUSK(u, (BookmarkItem)bookmark)) {
this.node.uskManager.unsubscribe(u, this.uskCB);
}
} catch(MalformedURLException mue) {
}
}
}
getCategoryByPath(parentPath(path)).removeBookmark(bookmark);
synchronized(bookmarks) {
bookmarks.remove(path);
}
}
private boolean wantUSK(USK u, BookmarkItem ignore) {
List<BookmarkItem> items = MAIN_CATEGORY.getAllItems();
for(BookmarkItem item : items) {
if(item == ignore)
continue;
if(!"USK".equals(item.getKeyType()))
continue;
try {
FreenetURI furi = new FreenetURI(item.getKey());
USK usk = USK.create(furi);
if(usk.equals(u, false)) return true;
} catch(MalformedURLException mue) {
}
}
return false;
}
public void moveBookmarkUp(String path, boolean store) {
BookmarkCategory parent = getCategoryByPath(parentPath(path));
parent.moveBookmarkUp(getBookmarkByPath(path));
if(store)
storeBookmarks();
}
public void moveBookmarkDown(String path, boolean store) {
BookmarkCategory parent = getCategoryByPath(parentPath(path));
parent.moveBookmarkDown(getBookmarkByPath(path));
if(store)
storeBookmarks();
}
private void putPaths(String path, Bookmark b) {
synchronized(bookmarks) {
bookmarks.put(path, b);
}
if(b instanceof BookmarkCategory)
for(int i = 0; i < ((BookmarkCategory) b).size(); i++) {
Bookmark child = ((BookmarkCategory) b).get(i);
putPaths(path + child.getName() + (child instanceof BookmarkItem ? "" : "/"), child);
}
}
private void removePaths(String path) {
if(getBookmarkByPath(path) instanceof BookmarkCategory) {
BookmarkCategory cat = getCategoryByPath(path);
for(int i = 0; i < cat.size(); i++)
removePaths(path + cat.get(i).getName() + (cat.get(i) instanceof BookmarkCategory ? "/" : ""));
}
bookmarks.remove(path);
}
public FreenetURI[] getBookmarkURIs() {
List<BookmarkItem> items = MAIN_CATEGORY.getAllItems();
FreenetURI[] uris = new FreenetURI[items.size()];
for(int i = 0; i < items.size(); i++)
uris[i] = items.get(i).getURI();
return uris;
}
public void storeBookmarksLazy() {
synchronized(bookmarks) {
if(isSavingBookmarksLazy) return;
isSavingBookmarksLazy = true;
node.node.ticker.queueTimedJob(new Runnable() {
@Override
public void run() {
try {
storeBookmarks();
} finally {
isSavingBookmarksLazy = false;
}
}
}, MINUTES.toMillis(5));
}
}
public void storeBookmarks() {
Logger.normal(this, "Attempting to save bookmarks to " + bookmarksFile.toString());
SimpleFieldSet sfs = null;
synchronized(bookmarks) {
if(isSavingBookmarks)
return;
isSavingBookmarks = true;
sfs = toSimpleFieldSet();
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(backupBookmarksFile);
sfs.writeToBigBuffer(fos);
fos.close();
fos = null;
if(!FileUtil.renameTo(backupBookmarksFile, bookmarksFile))
Logger.error(this, "Unable to rename " + backupBookmarksFile.toString() + " to " + bookmarksFile.toString());
} catch(IOException ioe) {
Logger.error(this, "An error has occured saving the bookmark file :" + ioe.getMessage(), ioe);
} finally {
Closer.close(fos);
synchronized(bookmarks) {
isSavingBookmarks = false;
}
}
}
private void readBookmarks(BookmarkCategory category, SimpleFieldSet sfs) {
_innerReadBookmarks("", category, sfs);
}
static final short PRIORITY = RequestStarter.BULK_SPLITFILE_PRIORITY_CLASS;
static final short PRIORITY_PROGRESS = RequestStarter.UPDATE_PRIORITY_CLASS;
private void subscribeToUSK(BookmarkItem item) {
if("USK".equals(item.getKeyType()))
try {
USK u = item.getUSK();
this.node.uskManager.subscribe(u, this.uskCB, true, this);
} catch(MalformedURLException mue) {}
}
private synchronized void _innerReadBookmarks(String prefix, BookmarkCategory category, SimpleFieldSet sfs) {
boolean hasBeenParsedWithoutAnyProblem = true;
boolean isRoot = ("".equals(prefix) && MAIN_CATEGORY.equals(category));
synchronized(bookmarks) {
if(!isRoot)
putPaths(prefix + category.name + '/', category);
try {
int nbBookmarks = sfs.getInt(BookmarkItem.NAME);
int nbCategories = sfs.getInt(BookmarkCategory.NAME);
for(int i = 0; i < nbBookmarks; i++) {
SimpleFieldSet subset = sfs.getSubset(BookmarkItem.NAME + i);
try {
BookmarkItem item = new BookmarkItem(subset, node.alerts);
String name = (isRoot ? "" : prefix + category.name) + '/' + item.name;
putPaths(name, item);
category.addBookmark(item);
subscribeToUSK(item);
} catch(MalformedURLException e) {
throw new FSParseException(e);
}
}
for(int i = 0; i < nbCategories; i++) {
SimpleFieldSet subset = sfs.getSubset(BookmarkCategory.NAME + i);
BookmarkCategory currentCategory = new BookmarkCategory(subset);
category.addBookmark(currentCategory);
String name = (isRoot ? "/" : (prefix + category.name + '/'));
_innerReadBookmarks(name, currentCategory, subset.getSubset("Content"));
}
} catch(FSParseException e) {
Logger.error(this, "Error parsing the bookmarks file!", e);
hasBeenParsedWithoutAnyProblem = false;
}
}
if(hasBeenParsedWithoutAnyProblem)
storeBookmarks();
}
public SimpleFieldSet toSimpleFieldSet() {
SimpleFieldSet sfs = new SimpleFieldSet(true);
sfs.put("Version", 1);
synchronized(bookmarks) {
sfs.putAllOverwrite(BookmarkManager.toSimpleFieldSet(MAIN_CATEGORY));
}
return sfs;
}
public static SimpleFieldSet toSimpleFieldSet(BookmarkCategory cat) {
SimpleFieldSet sfs = new SimpleFieldSet(true);
List<BookmarkCategory> bc = cat.getSubCategories();
for(int i = 0; i < bc.size(); i++) {
BookmarkCategory currentCat = bc.get(i);
sfs.put(BookmarkCategory.NAME + i, currentCat.getSimpleFieldSet());
}
sfs.put(BookmarkCategory.NAME, bc.size());
List<BookmarkItem> bi = cat.getItems();
for(int i = 0; i < bi.size(); i++)
sfs.put(BookmarkItem.NAME + i, bi.get(i).getSimpleFieldSet());
sfs.put(BookmarkItem.NAME, bi.size());
return sfs;
}
@Override
public boolean persistent() {
return false;
}
@Override
public boolean realTimeFlag() {
return false;
}
}