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    }