001 /*
002 * This file is part of muCommander, http://www.mucommander.com
003 * Copyright (C) 2002-2008 Maxence Bernard
004 *
005 * muCommander is free software; you can redistribute it and/or modify
006 * it under the terms of the GNU General Public License as published by
007 * the Free Software Foundation; either version 3 of the License, or
008 * (at your option) any later version.
009 *
010 * muCommander is distributed in the hope that it will be useful,
011 * but WITHOUT ANY WARRANTY; without even the implied warranty of
012 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
013 * GNU General Public License for more details.
014 *
015 * You should have received a copy of the GNU General Public License
016 * along with this program. If not, see <http://www.gnu.org/licenses/>.
017 */
018
019 package com.mucommander.bookmark;
020
021 import com.mucommander.PlatformManager;
022 import com.mucommander.file.AbstractFile;
023 import com.mucommander.file.FileFactory;
024 import com.mucommander.io.BackupInputStream;
025 import com.mucommander.io.BackupOutputStream;
026 import com.mucommander.util.AlteredVector;
027 import com.mucommander.util.VectorChangeListener;
028
029 import java.io.*;
030 import java.util.Iterator;
031 import java.util.WeakHashMap;
032
033 /**
034 * This class manages the boomark list and its parsing and storage as an XML file.
035 * <p>
036 * It monitors any changes made to the bookmarks and when changes are made, fires change events to registered
037 * listeners.
038 * </p>
039 * @author Maxence Bernard, Nicolas Rinaudo
040 */
041 public class BookmarkManager implements VectorChangeListener {
042 /** Whether we're currently loading the bookmarks or not. */
043 private static boolean isLoading = false;
044
045 /** Bookmarks file location */
046 private static AbstractFile bookmarksFile;
047
048 /** Default bookmarks file name */
049 private static final String DEFAULT_BOOKMARKS_FILE_NAME = "bookmarks.xml";
050
051 /** Bookmark instances */
052 private static AlteredVector bookmarks = new AlteredVector();
053
054 /** Contains all registered bookmark listeners, stored as weak references */
055 private static WeakHashMap listeners = new WeakHashMap();
056
057 /** Specifies whether bookmark events should be fired when a change to the bookmarks is detected */
058 private static boolean fireEvents = true;
059
060 /** True when changes were made after the bookmarks file was last saved */
061 private static boolean saveNeeded;
062
063 /** Last bookmark change timestamp */
064 private static long lastBookmarkChangeTime;
065
066 /** Last event pause timestamp */
067 private static long lastEventPauseTime;
068
069 /** Create a singleton instance, needs to be referenced so that it's not garbage collected (AlteredVector
070 * stores VectorChangeListener as weak references) */
071 private static BookmarkManager singleton = new BookmarkManager();
072
073
074
075 // - Initialisation --------------------------------------------------------
076 // -------------------------------------------------------------------------
077 static {
078 // Listen to changes made to the bookmarks vector
079 bookmarks.addVectorChangeListener(singleton);
080 }
081
082 /**
083 * Prevents instanciation of <code>BookmarkManager</code>.
084 */
085 private BookmarkManager() {}
086
087
088
089 // - Bookmark building -----------------------------------------------------
090 // -------------------------------------------------------------------------
091 /**
092 * Passes messages about all known bookmarks to the specified builder.
093 * @param builder where to send bookmark building messages.
094 * @throws BookmarkException if an error occurs.
095 */
096 public static synchronized void buildBookmarks(BookmarkBuilder builder) throws BookmarkException {
097 Iterator iterator;
098 Bookmark bookmark;
099
100 builder.startBookmarks();
101 iterator = bookmarks.iterator();
102 while(iterator.hasNext()) {
103 bookmark = (Bookmark)iterator.next();
104 builder.addBookmark(bookmark.getName(), bookmark.getLocation());
105 }
106 builder.endBookmarks();
107 }
108
109
110
111 // - Bookmark file access --------------------------------------------------
112 // -------------------------------------------------------------------------
113 /**
114 * Returns the path to the bookmark file.
115 * <p>
116 * If it hasn't been changed through a call to {@link #setBookmarksFile(String)},
117 * this method will return the default, system dependant bookmarks file.
118 * </p>
119 * @return the path to the bookmark file.
120 * @see #setBookmarksFile(String)
121 * @throws IOException if there was a problem locating the default bookmarks file.
122 */
123 public static synchronized AbstractFile getBookmarksFile() throws IOException {
124 if(bookmarksFile == null)
125 return PlatformManager.getPreferencesFolder().getChild(DEFAULT_BOOKMARKS_FILE_NAME);
126 return bookmarksFile;
127 }
128
129 /**
130 * Sets the path to the bookmarks file.
131 * <p>
132 * This is a convenience method and is strictly equivalent to calling <code>setBookmarksFile(FileFactory.getFile(file))</code>.
133 * </p>
134 * @param path path to the bookmarks file
135 * @exception FileNotFoundException if <code>path</code> is not accessible.
136 * @see #getBookmarksFile()
137 */
138 public static void setBookmarksFile(String path) throws FileNotFoundException {
139 AbstractFile file;
140
141 if((file = FileFactory.getFile(path)) == null)
142 setBookmarksFile(new File(path));
143 else
144 setBookmarksFile(file);
145 }
146
147 /**
148 * Sets the path to the bookmarks file.
149 * <p>
150 * This is a convenience method and is strictly equivalent to calling <code>setBookmarksFile(FileFactory.getFile(file.getAbsolutePath()))</code>.
151 * </p>
152 * @param file path to the bookmarks file
153 * @exception FileNotFoundException if <code>path</code> is not accessible.
154 * @see #getBookmarksFile()
155 */
156 public static void setBookmarksFile(File file) throws FileNotFoundException {setBookmarksFile(FileFactory.getFile(file.getAbsolutePath()));}
157
158 /**
159 * Sets the path to the bookmarks file.
160 * @param file path to the bookmarks file
161 * @exception FileNotFoundException if <code>path</code> is not accessible.
162 * @see #getBookmarksFile()
163 */
164
165 public static synchronized void setBookmarksFile(AbstractFile file) throws FileNotFoundException {
166 if(file.isBrowsable())
167 throw new FileNotFoundException("Not a valid file: " + file.getAbsolutePath());
168 bookmarksFile = file;
169 }
170
171
172
173 // - Bookmarks loading -----------------------------------------------------
174 // -------------------------------------------------------------------------
175 /**
176 * Loads all available bookmarks.
177 * @throws Exception if an error occurs.
178 */
179 public static synchronized void loadBookmarks() throws Exception {
180 InputStream in;
181
182 // Parse the bookmarks file
183 in = null;
184 isLoading = true;
185 try {readBookmarks(in = new BackupInputStream(getBookmarksFile()), new Loader());}
186 finally {
187 if(in != null) {
188 try {in.close();}
189 catch(Exception e) {}
190 }
191 isLoading = false;
192 }
193 }
194
195 /**
196 * Reads bookmarks from the specified <code>InputStream</code>.
197 * @param in where to read bookmarks from.
198 * @throws Exception if an error occurs.
199 */
200 public static void readBookmarks(InputStream in) throws Exception {readBookmarks(in, new Loader());}
201
202 /**
203 * Reads bookmarks from the specified <code>InputStream</code> and passes messages to the specified {@link BookmarkBuilder}.
204 * @param in where to read bookmarks from.
205 * @param builder where to send builing messages to.
206 * @throws Exception if an error occurs.
207 */
208 public static synchronized void readBookmarks(InputStream in, BookmarkBuilder builder) throws Exception {new BookmarkParser().parse(in, builder);}
209
210
211
212 // - Bookmarks writing -----------------------------------------------------
213 // -------------------------------------------------------------------------
214 /**
215 * Returns a {@link BookmarkBuilder} that will write all building messages as XML to the specified output stream.
216 * @param out where to write the bookmarks' XML content.
217 * @return a {@link BookmarkBuilder} that will write all building messages as XML to the specified output stream.
218 * @throws IOException if an IO related error occurs.
219 */
220 public static BookmarkBuilder getBookmarkWriter(OutputStream out) throws IOException {return new BookmarkWriter(out);}
221
222 /**
223 * Writes all known bookmarks to the bookmark {@link #getBookmarksFile() file}.
224 * @param forceWrite if false, the bookmarks file will be written only if changes were made to bookmarks since
225 * last write, if true the file will always be written
226 * @throws IOException if an I/O error occurs.
227 * @throws BookmarkException if an error occurs.
228 */
229 public static synchronized void writeBookmarks(boolean forceWrite) throws IOException, BookmarkException {
230 OutputStream out;
231
232 // Write bookmarks file only if changes were made to the bookmarks since last write, or if write is forced.
233 if(!(forceWrite || saveNeeded))
234 return;
235 out = null;
236 try {
237 buildBookmarks(getBookmarkWriter(out = new BackupOutputStream(getBookmarksFile())));
238 saveNeeded = false;
239 }
240 finally {
241 if(out != null) {
242 try {out.close();}
243 catch(Exception e) {}
244 }
245 }
246 }
247
248
249
250 // - Bookmarks access ------------------------------------------------------
251 // -------------------------------------------------------------------------
252 /**
253 * Returns an {@link AlteredVector} that contains all bookmarks.
254 *
255 * <p>Important: the returned Vector should not directly be used to
256 * add or remove bookmarks, doing so won't trigger any event to registered bookmark listeners.
257 * However, it is safe to modify bookmarks individually, events will be properly fired.
258 * @return an {@link AlteredVector} that contains all bookmarks.
259 */
260 public static synchronized AlteredVector getBookmarks() {
261 return bookmarks;
262 }
263
264 /**
265 * Deletes the specified bookmark.
266 * @param bookmark bookmark to delete from the list.
267 */
268 public static synchronized void removeBookmark(Bookmark bookmark) {bookmarks.remove(bookmark);}
269
270 /**
271 * Convenience method that looks for a Bookmark with the given name (case ignored) and returns it,
272 * or null if none was found. If several bookmarks have the given name, the first one is returned.
273 *
274 * @param name the bookmark's name
275 * @return a Bookmark instance with the given name, null if none was found
276 */
277 public static synchronized Bookmark getBookmark(String name) {
278 int nbBookmarks = bookmarks.size();
279 Bookmark b;
280 for(int i=0; i<nbBookmarks; i++) {
281 b = (Bookmark)bookmarks.elementAt(i);
282 if(b.getName().equalsIgnoreCase(name))
283 return b;
284 }
285
286 return null;
287 }
288
289 /**
290 * Convenience method that adds a bookmark to the bookmark list.
291 *
292 * @param b the Bookmark instance to add to the bookmark list.
293 */
294 public static synchronized void addBookmark(Bookmark b) {bookmarks.add(b);}
295
296
297
298 // - Listeners -------------------------------------------------------------
299 // -------------------------------------------------------------------------
300 /**
301 * Adds the specified BookmarkListener to the list of registered listeners.
302 *
303 * <p>Listeners are stored as weak references so {@link #removeBookmarkListener(BookmarkListener)}
304 * doesn't need to be called for listeners to be garbage collected when they're not used anymore.
305 *
306 * @param listener the BookmarkListener to add to the list of registered listeners.
307 * @see #removeBookmarkListener(BookmarkListener)
308 */
309 public static void addBookmarkListener(BookmarkListener listener) {synchronized(listeners) {listeners.put(listener, null);}}
310
311 /**
312 * Removes the specified BookmarkListener from the list of registered listeners.
313 *
314 * @param listener the BookmarkListener to remove from the list of registered listeners.
315 * @see #addBookmarkListener(BookmarkListener)
316 */
317 public static void removeBookmarkListener(BookmarkListener listener) {synchronized(listeners) {listeners.remove(listener);}}
318
319 /**
320 * Notifies all the registered bookmark listeners of a bookmark change. This can be :
321 * <ul>
322 * <li>A new bookmark which has just been added
323 * <li>An existing bookmark which has been modified
324 * <li>An existing bookmark which has been removed
325 * </ul>
326 */
327 public static void fireBookmarksChanged() {
328 // Bookmarks file will need to be saved
329 if(!isLoading)
330 saveNeeded = true;
331
332 lastBookmarkChangeTime = System.currentTimeMillis();
333
334 // Do not fire event if events are currently disabled
335 if(!fireEvents)
336 return;
337
338 synchronized(listeners) {
339 // Iterate on all listeners
340 Iterator iterator = listeners.keySet().iterator();
341 while(iterator.hasNext())
342 ((BookmarkListener)iterator.next()).bookmarksChanged();
343 }
344 }
345
346 /**
347 * Specifies whether bookmark events should be fired when a change in the bookmarks is detected. This allows
348 * to temporarily suspend events firing when a lot of them are made, for example when editing the bookmarks list.
349 *
350 * <p>If true is speicified, any subsequent calls to fireBookmarksChanged will be ignored, until this method is
351 * called again with false.</p>
352 * @param b whether to fire events.
353 */
354 public static synchronized void setFireEvents(boolean b) {
355 if(b) {
356 // Fire a bookmarks changed event if bookmarks were modified during event pause
357 if(!fireEvents && lastBookmarkChangeTime >= lastEventPauseTime) {
358 fireEvents = true;
359 fireBookmarksChanged();
360 }
361 }
362 else {
363 // Remember pause start time
364 if(fireEvents) {
365 fireEvents = false;
366 lastEventPauseTime = System.currentTimeMillis();
367 }
368 }
369 }
370
371 /////////////////////////////////////////
372 // VectorChangeListener implementation //
373 /////////////////////////////////////////
374
375 public void elementsAdded(int startIndex, int nbAdded) {
376 fireBookmarksChanged();
377 }
378
379 public void elementsRemoved(int startIndex, int nbRemoved) {
380 fireBookmarksChanged();
381 }
382
383 public void elementChanged(int index) {
384 fireBookmarksChanged();
385 }
386
387
388
389 // - Bookmark loading ------------------------------------------------------
390 // -------------------------------------------------------------------------
391 private static class Loader implements BookmarkBuilder {
392 public void startBookmarks() {}
393 public void endBookmarks() {}
394 public void addBookmark(String name, String location) {BookmarkManager.addBookmark(new Bookmark(name, location));}
395 }
396 }