001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.openfile;
043:
044: import java.beans.PropertyChangeEvent;
045: import java.util.prefs.BackingStoreException;
046: import org.netbeans.modules.openfile.RecentFiles.HistoryItem;
047: import org.openide.loaders.DataObject;
048: import org.openide.windows.CloneableTopComponent;
049: import org.openide.windows.TopComponent;
050: import java.beans.PropertyChangeListener;
051: import java.lang.NumberFormatException;
052: import java.net.MalformedURLException;
053: import java.net.URL;
054: import java.util.ArrayList;
055: import java.util.Collections;
056: import java.util.List;
057: import java.util.logging.Level;
058: import java.util.logging.Logger;
059: import java.util.prefs.Preferences;
060: import org.openide.filesystems.FileObject;
061: import org.openide.filesystems.URLMapper;
062: import org.openide.util.NbPreferences;
063: import org.openide.windows.WindowManager;
064:
065: /**
066: * Manages prioritized set of recently closed files.
067: *
068: * @author Dafe Simonek
069: */
070: public final class RecentFiles {
071:
072: /** List of recently closed files */
073: private static List<HistoryItem> history = new ArrayList<HistoryItem>();
074:
075: /** Preferences node for storing history info */
076: private static Preferences prefs;
077:
078: private static final Object HISTORY_LOCK = new Object();
079:
080: /** Name of preferences node where we persist history */
081: private static final String PREFS_NODE = "RecentFilesHistory";
082:
083: /** Separator to encode file path and time into one string in preferences */
084: private static final String SEPARATOR = "; time=";
085:
086: /** Boundary for items count in history */
087: private static final int MAX_HISTORY_ITEMS = 20;
088:
089: private RecentFiles() {
090: }
091:
092: /** Starts to listen for recently closed files */
093: public static void init() {
094: WindowManager.getDefault().invokeWhenUIReady(new Runnable() {
095: public void run() {
096: List<HistoryItem> loaded = load();
097: synchronized (HISTORY_LOCK) {
098: history.addAll(0, loaded);
099: }
100: TopComponent.getRegistry().addPropertyChangeListener(
101: new WindowRegistryL());
102: }
103: });
104: }
105:
106: /** Returns read-only list of recently closed files */
107: public static List<HistoryItem> getRecentFiles() {
108: synchronized (HISTORY_LOCK) {
109: checkHistory();
110: return Collections.unmodifiableList(history);
111: }
112: }
113:
114: /** Loads list of recent files stored in previous system sessions.
115: * @return list of stored recent files
116: */
117: static List<HistoryItem> load() {
118: String[] keys;
119: Preferences prefs = getPrefs();
120: try {
121: keys = prefs.keys();
122: } catch (BackingStoreException ex) {
123: Logger.getLogger(RecentFiles.class.getName()).log(
124: Level.FINE, ex.getMessage(), ex);
125: return Collections.emptyList();
126: }
127: List<HistoryItem> result = new ArrayList<HistoryItem>(
128: keys.length + 10);
129: HistoryItem hItem;
130: for (String curKey : keys) {
131: hItem = decode(prefs.get(curKey, null));
132: if (hItem != null) {
133: result.add(hItem);
134: } else {
135: // decode failed, so clear crippled item
136: prefs.remove(curKey);
137: }
138: }
139: Collections.sort(result);
140: return result;
141: }
142:
143: private static HistoryItem decode(String value) {
144: int sepIndex = value.lastIndexOf(SEPARATOR);
145: if (sepIndex <= 0) {
146: return null;
147: }
148: URL url = null;
149: try {
150: url = new URL(value.substring(0, sepIndex));
151: } catch (MalformedURLException ex) {
152: // url corrupted, skip
153: Logger.getLogger(RecentFiles.class.getName()).log(
154: Level.FINE, ex.getMessage(), ex);
155: return null;
156: }
157: long time = 0;
158: try {
159: time = Long.decode(value.substring(sepIndex
160: + SEPARATOR.length()));
161: } catch (NumberFormatException ex) {
162: // stored data corrupted, skip
163: Logger.getLogger(RecentFiles.class.getName()).log(
164: Level.FINE, ex.getMessage(), ex);
165: return null;
166: }
167: return new HistoryItem(url, time);
168: }
169:
170: static void storeRemoved(HistoryItem hItem) {
171: String stringURL = hItem.getURL().toExternalForm();
172: getPrefs().remove(trimToKeySize(stringURL));
173: }
174:
175: static void storeAdded(HistoryItem hItem) {
176: String stringURL = hItem.getURL().toExternalForm();
177: String value = stringURL + SEPARATOR
178: + String.valueOf(hItem.getTime());
179: getPrefs().put(trimToKeySize(stringURL), value);
180: }
181:
182: private static String trimToKeySize(String path) {
183: int length = path.length();
184: if (length > Preferences.MAX_KEY_LENGTH) {
185: path = path.substring(length - Preferences.MAX_KEY_LENGTH,
186: length);
187: }
188: return path;
189: }
190:
191: static Preferences getPrefs() {
192: if (prefs == null) {
193: prefs = NbPreferences.forModule(RecentFiles.class).node(
194: PREFS_NODE);
195: }
196: return prefs;
197: }
198:
199: /** Adds file represented by given TopComponent to the list,
200: * if conditions are met.
201: */
202: private static void addFile(TopComponent tc) {
203: if (tc instanceof CloneableTopComponent) {
204: URL fileURL = obtainURL(tc);
205: if (fileURL != null) {
206: boolean added = false;
207: synchronized (HISTORY_LOCK) {
208: // avoid duplicates
209: HistoryItem hItem = findHistoryItem(fileURL);
210: if (hItem == null) {
211: hItem = new HistoryItem(fileURL, System
212: .currentTimeMillis());
213: history.add(0, hItem);
214: storeAdded(hItem);
215: added = true;
216: // keep manageable size of history
217: // remove the oldest item if needed
218: if (history.size() > MAX_HISTORY_ITEMS) {
219: HistoryItem oldest = history.get(history
220: .size() - 1);
221: history.remove(oldest);
222: storeRemoved(oldest);
223: }
224: }
225: }
226: }
227: }
228: }
229:
230: /** Removes file represented by given TopComponent from the list */
231: private static void removeFile(TopComponent tc) {
232: if (tc instanceof CloneableTopComponent) {
233: URL fileURL = obtainURL(tc);
234: if (fileURL != null) {
235: boolean removed = false;
236: synchronized (HISTORY_LOCK) {
237: HistoryItem hItem = findHistoryItem(fileURL);
238: if (hItem != null) {
239: history.remove(hItem);
240: storeRemoved(hItem);
241: removed = true;
242: }
243: }
244: }
245: }
246: }
247:
248: private static URL obtainURL(TopComponent tc) {
249: DataObject dObj = tc.getLookup().lookup(DataObject.class);
250: if (dObj != null) {
251: FileObject fo = dObj.getPrimaryFile();
252: if (fo != null) {
253: return convertFile2URL(fo);
254: }
255: }
256: return null;
257: }
258:
259: private static HistoryItem findHistoryItem(URL url) {
260: for (HistoryItem hItem : history) {
261: if (url.equals(hItem.getURL())) {
262: return hItem;
263: }
264: }
265: return null;
266: }
267:
268: static URL convertFile2URL(FileObject fo) {
269: URL url = URLMapper.findURL(fo, URLMapper.EXTERNAL);
270: if (url == null) {
271: Logger.getLogger(RecentFiles.class.getName()).log(
272: Level.FINE,
273: "convertFile2URL: URL can't be found for FileObject "
274: + fo); // NOI18N
275: }
276: return url;
277: }
278:
279: static FileObject convertURL2File(URL url) {
280: FileObject fo = URLMapper.findFileObject(url);
281: if (fo == null) {
282: Logger.getLogger(RecentFiles.class.getName()).log(
283: Level.FINE,
284: "convertURL2File: File can't be found for URL "
285: + url); // NOI18N
286: }
287: return fo;
288: }
289:
290: /** Checks recent files history and removes non-valid entries */
291: private static void checkHistory() {
292: // note, code optimized for the frequent case that there are no invalid entries
293: List<HistoryItem> invalidEntries = new ArrayList<HistoryItem>(3);
294: FileObject fo = null;
295: for (HistoryItem historyItem : history) {
296: fo = convertURL2File(historyItem.getURL());
297: if (fo == null || !fo.isValid()) {
298: invalidEntries.add(historyItem);
299: }
300: }
301: for (HistoryItem historyItem : invalidEntries) {
302: history.remove(historyItem);
303: }
304: }
305:
306: /** One item of the recently closed files history
307: * Comparable by the time field, ascending from most recent to older items.
308: */
309: public static final class HistoryItem<T extends HistoryItem>
310: implements Comparable<T> {
311:
312: private long time;
313: private URL fileURL;
314:
315: HistoryItem(URL fileURL, long time) {
316: this .fileURL = fileURL;
317: this .time = time;
318: }
319:
320: public URL getURL() {
321: return fileURL;
322: }
323:
324: public long getTime() {
325: return time;
326: }
327:
328: public int compareTo(T other) {
329: long diff = time - other.getTime();
330: return diff < 0 ? 1 : diff > 0 ? -1 : 0;
331: }
332:
333: }
334:
335: /** Receives info about opened and closed TopComponents from window system.
336: */
337: private static class WindowRegistryL implements
338: PropertyChangeListener {
339:
340: public void propertyChange(PropertyChangeEvent evt) {
341: if (TopComponent.Registry.PROP_TC_CLOSED.equals(evt
342: .getPropertyName())) {
343: addFile((TopComponent) evt.getNewValue());
344: }
345: if (TopComponent.Registry.PROP_TC_OPENED.equals(evt
346: .getPropertyName())) {
347: removeFile((TopComponent) evt.getNewValue());
348: }
349: }
350:
351: }
352:
353: }
|