package edu.colostate.vchill.bookmark; import edu.colostate.vchill.DialogUtil; import edu.colostate.vchill.ScaleManager; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreePath; import javax.xml.parsers.SAXParserFactory; import java.io.*; import java.util.ArrayList; import java.util.Collection; import java.util.Map; import java.util.TreeMap; /** * Controller class for VCHILL's bookmark module. * This module is synchronized. * * @author Jochen Deyke * @author Alexander Deyke * @author jpont * @version 2009-06-30 */ public class BookmarkControl { public static final String USER_PREFIX = "my:"; public static final ScaleManager sm = ScaleManager.getInstance(); private final Map<String, Map<String, Bookmark>> bookmarks; private final BookmarkTreeModel model; private static final BookmarkControl bmc = new BookmarkControl(); public static BookmarkControl getInstance() { return bmc; } /** * Private default constructor prevents instantiation */ private BookmarkControl() { this.bookmarks = new TreeMap<String, Map<String, Bookmark>>(); this.model = new BookmarkTreeModel(new DefaultMutableTreeNode("Bookmarks")); } /** * Selects the category matching <code>cat</code>. * Always returns a valid category - if it did not previously exist, it is created. * * @param cat String naming the desired category * @return The selected category */ private Map<String, Bookmark> selectCat(final String cat) { Map<String, Bookmark> tmp = this.bookmarks.get(cat); if (tmp == null) { tmp = new TreeMap<String, Bookmark>(); this.bookmarks.put(cat, tmp); this.model.insertNodeSorted(new DefaultMutableTreeNode(cat)); } return tmp; } /** * Add a new bookmark. The tree is also updated. * * @param cat The category to add the bookmark to * @param name The name for the new bookmark * @param bookmark The new bookmark itself */ public synchronized void addBookmark(final String cat, final String name, final Bookmark bookmark) { String fullname = name + " " + bookmark.scan_type; if (selectCat(cat).put(fullname, bookmark) == null) { this.model.insertNodeToCategory(cat, new DefaultMutableTreeNode(fullname)); } //only add to tree if actually new } /** * Retrieve a specific bookmark * * @param cat The same category the bookmark was added to * @param name The name as returned by getBookmarkList(<code>cat</code>) * @return The desired bookmark */ public synchronized Bookmark getBookmark(final String cat, final String name) { return selectCat(cat).get(name); } /** * Renames a category. If the named category does not exist, * nothing happens. The tree is <b>NOT</b> updated. * * @param from the old name of the category * @param to the new name for the category */ public synchronized void renameCategory(final String from, final String to) { Map<String, Bookmark> tmp = this.bookmarks.remove(from); if (tmp == null) return; //didn't exist this.bookmarks.put(to, tmp); } /** * Renames a bookmark. If the named bookmark does not exist, * nothing happens. The tree is <b>NOT</b> updated. * * @param cat the category of the bookmark to rename * @param from the old name of the bookmark * @param to the new name for the bookmark */ public synchronized void renameBookmark(final String cat, final String from, final String to) { Map<String, Bookmark> tmp = this.bookmarks.get(cat); if (tmp == null) return; //didn't exist Bookmark b = tmp.remove(from); if (b == null) return; //didn't exist tmp.put(to, b); } /** * Moves a bookmark from one category to another. * If the named bookmark does not exist, nothing happens. * The tree is <b>NOT</b> updated. * * @param cat the category of the bookmark to move * @param name the name of the bookmark to move * @param to the name of the new category for the bookmark. * If it did not exist before, it is created. */ public synchronized void moveBookmark(final String cat, final String name, final String to) { Map<String, Bookmark> from = this.bookmarks.get(cat); if (from == null) return; //didn't exist Bookmark b = from.remove(name); if (b == null) return; //didn't exist selectCat(to).put(name, b); } /** * Rename selected node. This can be either a bookmark or a catedory. * If the given path is neither a bookmark nor a category, do nothing. * * @param path the path to the node to rename */ public synchronized void rename(final TreePath path) { switch (path.getPathCount()) { case 3: //actual bookmark DefaultMutableTreeNode parent = (DefaultMutableTreeNode) path.getPathComponent(1); DefaultMutableTreeNode child = (DefaultMutableTreeNode) path.getPathComponent(2); String newBName = DialogUtil.showInputDialog("Rename Bookmark", "New name:", stripBookmarkName(child.toString())); if (newBName == null || newBName.length() < 1) return; newBName += " " + scanType(child.toString()); this.renameBookmark(parent.toString(), child.toString(), newBName); this.model.removeNodeFromParent(child); child.setUserObject(newBName); this.model.insertNodeSorted(parent, child); break; case 2: //category DefaultMutableTreeNode cat = (DefaultMutableTreeNode) path.getPathComponent(1); String newCName = DialogUtil.showInputDialog("Rename Category", "New name:", stripCategoryName(cat.toString())); if (newCName == null || newCName.length() < 1) return; newCName = USER_PREFIX + newCName; this.renameCategory(cat.toString(), newCName); this.model.removeNodeFromParent(cat); cat.setUserObject(newCName); this.model.insertNodeSorted(cat); break; } } /** * Export one or more nodes to a specified XML file. * If no paths are specified, everything is exported. * * @param to the file to save to * @param paths the path(s) to export */ public synchronized void export(final File to, final TreePath... paths) { if (paths == null || paths.length < 1) { this.save(to); return; } PrintStream file; Bookmark bookmark; try { file = new PrintStream(new BufferedOutputStream(new FileOutputStream(to)), false, "UTF-8"); } catch (Exception e) { System.err.println(e.toString()); file = System.out; } file.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); file.println("<bookmarks>"); for (TreePath path : paths) { DefaultMutableTreeNode cat = (DefaultMutableTreeNode) path.getPathComponent(1); String catName = cat.toString(); switch (path.getPathCount()) { case 3: //actual bookmark String bmName = path.getPathComponent(2).toString(); bookmark = selectCat(catName).get(bmName); writeBookmark(catName, bmName, bookmark, file); break; case 2: //category Map<String, Bookmark> category = this.bookmarks.get(catName); for (String name : category.keySet()) { //each bookmark bookmark = category.get(name); writeBookmark(catName, name, bookmark, file); } break; } } file.println("</bookmarks>"); file.flush(); file.close(); } /** * Gets a list of available categories * * @return A Collection of available category name Strings */ public synchronized Collection<String> getCategoryList() { return this.bookmarks.keySet(); } /** * Gets a list of available bookmarks in a given category * * @param category the category to list * @return a Collection of available bookmark name Strings in that category */ public synchronized Collection<String> getBookmarkList(final String category) { return selectCat(category).keySet(); } /** * Returns the model from which to generate the tree. * * @return the model */ public BookmarkTreeModel getModel() { return model; } /** * Load XML format bookmarks from a File */ public synchronized void load(final File file) { try { this.load(new BufferedInputStream(new FileInputStream(file)), USER_PREFIX); } catch (FileNotFoundException fnfe) { System.err.println("Error loading bookmarks from " + file + ":\nFile not found"); } } /** * Load XML format bookmarks from an InputStream */ public synchronized void load(final InputStream stream, final String prefix) { try { SAXParserFactory.newInstance().newSAXParser().parse(stream, new XMLBookmarkHandler(prefix)); } catch (Exception e) { System.err.println("Exception while loading bookmarks:"); e.printStackTrace(); } } /** * Save bookmarks to "bookmarks.xml" */ public synchronized void save() { this.save("bookmarks.xml"); } /** * Saves bookmarks to a given filename in XML format * * @param filename the name of the file to save to */ public synchronized void save(final String filename) { this.save(new File(filename)); } /** * Saves bookmarks to a given file in UTF-8 encoded XML format * * @param path the file to save to */ public synchronized void save(final File path) { PrintStream file; try { file = new PrintStream(new BufferedOutputStream(new FileOutputStream(path)), false, "UTF-8"); } catch (Exception e) { System.err.println(e.toString()); file = System.out; } file.println("<?xml version=\"1.0\" encoding=\"UTF-8\"?>"); file.println("<bookmarks>"); for (String cat : this.bookmarks.keySet()) { //each category if (!cat.startsWith(USER_PREFIX)) continue; //only save user bookmarks Map<String, Bookmark> category = this.bookmarks.get(cat); for (String name : category.keySet()) { //each bookmark Bookmark bookmark = category.get(name); writeBookmark(cat, name, bookmark, file); } } file.println("</bookmarks>"); file.flush(); file.close(); } /** * Writes a given bookmark to a file * * @param cat the name of the category * @param name the name of the bookmark * @param bookmark the actual bookmark to write * @param file the stream to write to */ public static void writeBookmark(final String cat, final String name, final Bookmark bookmark, final PrintStream file) { file.println(" <bookmark>"); file.println(" <category>" + stripCategoryName(cat) + "</category>"); //trim prefix file.println(" <name>" + stripBookmarkName(name) + "</name>"); file.println(" <url>" + bookmark.url + "</url>"); file.println(" <directory>" + bookmark.dir + "</directory>"); file.println(" <file>" + bookmark.file + "</file>"); file.println(" <sweep>" + bookmark.sweep + "</sweep>"); file.println(" <scantype>" + bookmark.scan_type + "</scantype>"); file.println(" <color>"); for (String type : bookmark.scale.keySet()) { file.println(" <" + type + ">"); Bookmark.Scale scale = bookmark.scale.get(type); file.println(" <autoscale>" + scale.autoscale + "</autoscale>"); file.println(" <minval>" + scale.minval + "</minval>"); file.println(" <maxval>" + scale.maxval + "</maxval>"); file.println(" </" + type + ">"); } file.println(" </color>"); file.println(" <pan>"); file.println(" <x>" + bookmark.x + "</x>"); file.println(" <y>" + bookmark.y + "</y>"); file.println(" </pan>"); file.println(" <range>" + bookmark.range + "</range>"); file.println(" <ring>" + bookmark.ring + "</ring>"); file.println(" <rhiheight>" + bookmark.rhi_height + "</rhiheight>"); file.println(" <comment>"); file.println(bookmark.comment.trim()); file.println(" </comment>"); file.println(" </bookmark>"); } /** * Removes one or more nodes from the tree. * If no path is specified, nothing is removed. * * @param paths the paths to the nodes to remove */ public synchronized void remove(final TreePath... paths) { if (paths == null) return; for (TreePath path : paths) { if (path == null) continue; switch (path.getPathCount()) { case 3: //actual bookmark DefaultMutableTreeNode parent = (DefaultMutableTreeNode) path.getPathComponent(1); Object child = path.getPathComponent(2); this.selectCat(parent.toString()).remove(child.toString()); this.model.removeNodeFromParent((DefaultMutableTreeNode) this.model.getChild(parent, this.model.getIndexOfChild(parent, child))); if (parent.getChildCount() == 0) this.model.removeNodeFromParent(parent); break; case 2: //category Object cat = path.getPathComponent(1); this.bookmarks.remove(cat.toString()); this.model.removeNodeFromParent((DefaultMutableTreeNode) this.model.getChild(this.model.getRoot(), this.model.getIndexOfChild(this.model.getRoot(), cat))); break; case 1: //root this.bookmarks.clear(); Object root = this.model.getRoot(); while (this.model.getChildCount(root) > 0) { this.model.removeNodeFromParent((DefaultMutableTreeNode) this.model.getChild(root, 0)); } break; } } } /** * Move one or more nodes into another category. * The user is prompted with a dialog for the new category to move to. * If no path is specified, do nothing. * If a category is specified, all bookmarks in that category are moved. * If this causes a category to become empty, the empty category is removed. * * @param paths the path(s) to the node(s) to move */ public synchronized void move(final TreePath... paths) { if (paths == null || paths.length < 1) return; Collection<String> cats = bmc.getCategoryList(); ArrayList<String> choices = new ArrayList<String>(cats.size()); for (String cat : cats) if (cat.startsWith(BookmarkControl.USER_PREFIX)) choices.add(stripCategoryName(cat)); String newCat = null; switch (paths[0].getPathCount()) { case 2: case 3: newCat = stripCategoryName(paths[0].getPathComponent(1).toString()); break; } if (newCat == null) newCat = DialogUtil.showOptionInputDialog("Move Bookmark(s)", "Category to move bookmark(s) to:", choices.toArray()); else newCat = DialogUtil.showOptionInputDialog("Move Bookmark(s)", "Category to move bookmark(s) to:", choices.toArray(), newCat); if (newCat == null || newCat.length() < 1) return; newCat = USER_PREFIX + newCat; for (TreePath path : paths) { if (path == null) continue; switch (path.getPathCount()) { case 3: //actual bookmark DefaultMutableTreeNode parent = (DefaultMutableTreeNode) path.getPathComponent(1); DefaultMutableTreeNode child = (DefaultMutableTreeNode) path.getPathComponent(2); this.moveBookmark(parent.toString(), child.toString(), newCat); this.model.removeNodeFromParent(child); if (parent.getChildCount() == 0) this.model.removeNodeFromParent(parent); this.model.insertNodeToCategory(newCat, child); break; case 2: //category DefaultMutableTreeNode cat = (DefaultMutableTreeNode) path.getPathComponent(1); if (newCat.equals(cat.toString())) continue; while (cat.getChildCount() > 0) { DefaultMutableTreeNode node = cat.getFirstLeaf(); this.moveBookmark(cat.toString(), node.toString(), newCat); this.model.removeNodeFromParent(node); this.model.insertNodeToCategory(newCat, node); } this.model.removeNodeFromParent(cat); break; } } } /** * Duplicates one or more nodes. * The new nodes are named the same as the original, but prefixed with "Copy of ". * * @param paths the path(s) to duplicate */ public synchronized void duplicate(final TreePath... paths) { for (TreePath path : paths) { Bookmark bookmark; switch (path.getPathCount()) { case 3: //actual bookmark String cat = path.getPathComponent(1).toString(); String name = path.getPathComponent(2).toString(); bookmark = new Bookmark(this.getBookmark(cat, name)); String fullname = "Copy of " + name; if (selectCat(cat).put(fullname, bookmark) == null) { this.model.insertNodeToCategory(cat, new DefaultMutableTreeNode(fullname)); } //only add to tree if actually new break; case 2: //category String catName = path.getPathComponent(1).toString(); Map<String, Bookmark> oldCat = selectCat(catName); Map<String, Bookmark> newCat = selectCat(catName = USER_PREFIX + "Copy of " + stripCategoryName(catName)); //fix name for (String bookmarkName : oldCat.keySet()) { bookmark = new Bookmark(oldCat.get(bookmarkName)); if (newCat.put(bookmarkName, bookmark) == null) { this.model.insertNodeToCategory(catName, new DefaultMutableTreeNode(bookmarkName)); } //only add to tree if actually new } break; } } } /** * Removes the prefix from a category name * * @param the category name to trim * @return the trimmed category name */ private static String stripCategoryName(final String cat) { return cat.substring(cat.indexOf(":") + 1, cat.length()); //trim prefix } /** * Removes the scan type from a bookmark name * * @param the bookmark name to trim * @return the trimmed bookmark name */ private static String stripBookmarkName(final String name) { return name.substring(0, name.lastIndexOf(" ")); //trim scan type } /** * Retrieves the scan type from a bookmark name * * @param the bookmark name to trim * @return the scan type */ private static String scanType(final String name) { return name.substring(name.lastIndexOf(" ") + 1, name.length()); //trim actual name } }