1 /* 2 * $Id$ 3 * 4 * Copyright (c) 2002, 2011, Oracle and/or its affiliates. All rights reserved. 5 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 6 * 7 * This code is free software; you can redistribute it and/or modify it 8 * under the terms of the GNU General Public License version 2 only, as 9 * published by the Free Software Foundation. Oracle designates this 10 * particular file as subject to the "Classpath" exception as provided 11 * by Oracle in the LICENSE file that accompanied this code. 12 * 13 * This code is distributed in the hope that it will be useful, but WITHOUT 14 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 15 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 16 * version 2 for more details (a copy is included in the LICENSE file that 17 * accompanied this code). 18 * 19 * You should have received a copy of the GNU General Public License version 20 * 2 along with this work; if not, write to the Free Software Foundation, 21 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 22 * 23 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 24 * or visit www.oracle.com if you need additional information or have any 25 * questions. 26 */ 27 28 package com.sun.javatest.tool; 29 30 import java.awt.event.ActionListener; 31 import java.io.*; 32 import java.lang.ref.WeakReference; 33 import java.nio.charset.StandardCharsets; 34 import java.util.*; 35 import javax.swing.JMenu; 36 import javax.swing.JMenuItem; 37 import javax.swing.event.MenuEvent; 38 import javax.swing.event.MenuListener; 39 40 import com.sun.javatest.WorkDirectory; 41 import com.sun.javatest.util.I18NResourceBundle; 42 43 /** 44 * A class to maintain a history of recently used files. The history is 45 * maintained in a specified file in a WorkDirectory, and can be 46 * dynamically added to a menu by means of a Listener class. 47 * The format of the file is one file per line, with most recently 48 * added entries appearing first. Lines beginning with <code>#</code> are ignored. 49 */ 50 public class FileHistory 51 { 52 /** 53 * Get a shared FileHistory object for a specified file and work directory. 54 * @param wd The work directory in which the history file is maintained. 55 * @param name The name of the file within the work direectory's jtData/ 56 * subdirectory. 57 * @return the specified FileHistory object 58 */ 59 public static FileHistory getFileHistory(WorkDirectory wd, String name) { 60 if (cache == null) 61 cache = new WeakHashMap<>(8); 62 63 // first, get a map for all files in this wd 64 Map<String, FileHistory> map = cache.get(wd); 65 if (map == null) { 66 map = new HashMap<>(8); 67 cache.put(wd, map); 68 } 69 70 // then, get the FileHistory for the specified file 71 FileHistory h = map.get(name); 72 if (h == null) { 73 h = new FileHistory(wd, name); 74 map.put(name, h); 75 } 76 77 return h; 78 } 79 80 81 /** 82 * Get a shared FileHistory object for a specified file and path to work directory. 83 * @param wdFile The path th work directory in which the history file is maintained. 84 * @param name The name of the file within the work direectory's jtData/ 85 * subdirectory. 86 * @return the specified FileHistory object 87 */ 88 public static FileHistory getFileHistory(File wdFile, String name) { 89 if (cache == null) 90 cache = new WeakHashMap<>(8); 91 92 if (!WorkDirectory.isWorkDirectory(wdFile)) 93 return null; 94 95 // let's find in the cache work dir corresponding to the path 96 Iterator<WorkDirectory> it = cache.keySet().iterator(); 97 WorkDirectory wd = null; 98 while (it.hasNext()) { 99 WorkDirectory tempWD = it.next(); 100 if (tempWD.getRoot().equals(wdFile)) { 101 wd = tempWD; 102 break; 103 } 104 } 105 if (wd != null) 106 return FileHistory.getFileHistory(wd, name); 107 else 108 return null; 109 } 110 111 /** 112 * Add a new file to the history. 113 * The file in the work directory for this history will be updated. 114 * @param file the file to be added to the history 115 */ 116 public void add(File file) { 117 ensureEntriesUpToDate(); 118 119 file = file.getAbsoluteFile(); 120 entries.remove(file); 121 entries.add(0, file); 122 123 writeEntries(); 124 } 125 126 /** 127 * Get the most recent entries from the history. Only entries for 128 * files that exist on this system are returned. Thus the history 129 * can accommodate files for different systems, which will likely not 130 * exist on all systems on which the history is used. 131 * @param count the number of most recent, existing files 132 * to be returned. 133 * @return an array of the most recent, existing entries 134 */ 135 public File[] getRecentEntries(int count) { 136 ensureEntriesUpToDate(); 137 138 // scan the entries, skipping those which do not exist, 139 // collecting up to count entries. Non-existent entries are 140 // skipped but not deleted because they might be for other 141 // platforms. 142 Vector<File> v = new Vector<>(); 143 for (int i = 0; i < entries.size() && v.size() < count; i++) { 144 File f = entries.elementAt(i); 145 if (f.exists()) 146 v.add(f); 147 } 148 File[] e = new File[v.size()]; 149 v.copyInto(e); 150 151 return e; 152 } 153 154 /** 155 * Get the latest valid entry from a file history object. An entry 156 * is valid if it identifies a file that exists on the current system. 157 * @return the latest valid entry from afile history object, or null 158 * if none found. 159 */ 160 public File getLatestEntry() { 161 ensureEntriesUpToDate(); 162 163 // scan the entries, skipping those which do not exist, 164 // looking for the first entry. Non-existent entries are 165 // skipped but not deleted because they might be for other 166 // platforms. 167 for (int i = 0; i < entries.size(); i++) { 168 File f = entries.elementAt(i); 169 if (f.exists()) 170 return f; 171 } 172 173 return null; 174 } 175 176 public File getRelativeLatestEntry(String newRoot, String oldRoot) { 177 ensureEntriesUpToDate(); 178 179 for (int i = 0; i < entries.size(); i++) { 180 File f = entries.elementAt(i); 181 if (f.exists()) { 182 return f; 183 } else { 184 String sf = f.getPath(); 185 String[] diff = WorkDirectory.getDiffInPaths(newRoot, oldRoot); 186 if (diff != null) { 187 File toCheck = new File(diff[0] + sf.substring(diff[1].length())); 188 if (toCheck.exists()) { 189 return toCheck; 190 } 191 } 192 193 } 194 } 195 return null; 196 } 197 198 199 private FileHistory(WorkDirectory workDir, String name) { 200 workDirRef = new WeakReference<>(workDir); // just used for logging errors 201 this.name = name; 202 historyFile = workDir.getSystemFile(name); 203 } 204 205 private void ensureEntriesUpToDate() { 206 if (entries == null || historyFile.lastModified() > historyFileLastModified) 207 readEntries(); 208 } 209 210 private void readEntries() { 211 if (entries == null) 212 entries = new Vector<>(); 213 else 214 entries.clear(); 215 216 if (historyFile.exists()) { 217 try { 218 BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(historyFile), StandardCharsets.UTF_8)); 219 String line; 220 while ((line = br.readLine()) != null) { 221 String p = line.trim(); 222 if (p.length() == 0 || p.startsWith("#")) 223 continue; 224 entries.add(new File(p)); 225 } 226 br.close(); 227 } 228 catch (IOException e) { 229 WorkDirectory workDir = workDirRef.get(); 230 workDir.log(i18n, "fh.cantRead", new Object[] { name, e } ); 231 } 232 233 historyFileLastModified = historyFile.lastModified(); 234 } 235 } 236 237 private void writeEntries() { 238 try { 239 BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(historyFile), StandardCharsets.UTF_8)); 240 bw.write("# Configuration File History"); 241 bw.newLine(); 242 bw.write("# written at " + (new Date())); 243 bw.newLine(); 244 for (int i = 0; i < entries.size(); i++) { 245 bw.write(entries.elementAt(i).toString()); 246 bw.newLine(); 247 } 248 bw.close(); 249 } 250 catch (IOException e) { 251 WorkDirectory workDir = workDirRef.get(); 252 workDir.log(i18n, "fh.cantWrite", new Object[] { name, e } ); 253 } 254 255 historyFileLastModified = historyFile.lastModified(); 256 } 257 258 259 /** 260 * A class that will dynamically add the latest entries for a 261 * FileHistory onto a menu. To do this, an instance of this class 262 * should be added to the menu with 263 * {@link javax.swing.JMenu#addMenuListener addMenuListener}. 264 */ 265 public static class Listener implements MenuListener { 266 /** 267 * Create a Listener that can be used to dynamically add the 268 * latest entries from a FileHistory onto a menu. The dynamic 269 * entries will be added to the end of the menu when it is 270 * selected. Any previous values added by this listener 271 * will automatically be removed. 272 * @param l An ActionListener that will be notified when 273 * any of the dynamic menu entries are invoked. When this 274 * action listener is notified, the action command will be 275 * the path of the file. The corresponding File object will 276 * be registered on the source as a client property named 277 * FILE. 278 */ 279 public Listener(ActionListener l) { 280 this(null, -1, l); 281 } 282 283 /** 284 * Create a Listener that can be used to dynamically add the 285 * latest entries from a FileHistory onto a menu. 286 * Any previous values added by this listener will automatically 287 * be removed. 288 * @param o The position in the menu at which to insert the 289 * dynamic entries. 290 * @param l An ActionListener that will be notified when 291 * any of the dynamic menu entries are invoked. When this 292 * action listener is notified, the action command will be 293 * the path of the file. The corresponding File object will 294 * be registered on the source as a client property named 295 * FILE. 296 */ 297 public Listener(int o, ActionListener l) { 298 this(null, o, l); 299 } 300 301 /** 302 * Create a Listener that can be used to dynamically add the 303 * latest entries from a FileHistory onto a menu. 304 * Any previous values added by this listener will automatically 305 * be removed. 306 * @param h The FileHistory from which to determine the 307 * entries to be added. 308 * @param o The position in the menu at which to insert the 309 * dynamic entries. 310 * @param l An ActionListener that will be notified when 311 * any of the dynamic menu entries are invoked. When this 312 * action listener is notified, the action command will be 313 * the path of the file. The corresponding File object will 314 * be registered on the source as a client property named 315 * FILE. 316 */ 317 public Listener(FileHistory h, int o, ActionListener l) { 318 history = h; 319 offset = o; 320 clientListener = l; 321 } 322 323 /** 324 * Get the FileHistory object from which to obtain the dynamic menu 325 * entries. 326 * @return the FileHistory object from which to obtain the dynamic menu 327 * entries 328 * @see #setFileHistory 329 */ 330 public FileHistory getFileHistory() { 331 return history; 332 } 333 334 /** 335 * Specify the FileHistory object from which to obtain the dynamic menu 336 * entries. 337 * @param h the FileHistory object from which to obtain the dynamic menu 338 * entries 339 * @see #getFileHistory 340 */ 341 public void setFileHistory(FileHistory h) { 342 history = h; 343 } 344 345 public void menuSelected(MenuEvent e) { 346 // Add the recent entries, or a disabled marker if none 347 JMenu menu = (JMenu) (e.getSource()); 348 File[] entries = (history == null ? null : history.getRecentEntries(5)); 349 if (entries == null || entries.length == 0) { 350 JMenuItem noEntries = new JMenuItem(i18n.getString("fh.empty")); 351 noEntries.putClientProperty(FILE_HISTORY, this); 352 noEntries.setEnabled(false); 353 if (offset < 0) 354 menu.add(noEntries); 355 else 356 menu.insert(noEntries, offset); 357 } 358 else { 359 for (int i = 0; i < entries.length; i++) { 360 JMenuItem mi = new JMenuItem(i + " " + entries[i].getPath()); 361 mi.setActionCommand(entries[i].getPath()); 362 mi.addActionListener(clientListener); 363 mi.putClientProperty(FILE, entries[i]); 364 mi.putClientProperty(FILE_HISTORY, this); 365 mi.setMnemonic('0' + i); 366 if (offset < 0) 367 menu.add(mi); 368 else 369 menu.insert(mi, offset + i); 370 } 371 } 372 } 373 374 public void menuDeselected(MenuEvent e) { 375 removeDynamicEntries((JMenu) (e.getSource())); 376 } 377 378 public void menuCanceled(MenuEvent e) { 379 removeDynamicEntries((JMenu) (e.getSource())); 380 } 381 382 private void removeDynamicEntries(JMenu menu) { 383 // Clear out any old menu items previously added by this 384 // menu listener; remove them from bottom up because 385 // removing an item affects index of subsequent items 386 for (int i = menu.getItemCount() -1; i >= 0; i--) { 387 JMenuItem mi = menu.getItem(i); 388 if (mi != null && mi.getClientProperty(FILE_HISTORY) == this) 389 menu.remove(mi); 390 } 391 } 392 393 private FileHistory history; 394 private int offset; 395 private ActionListener clientListener; 396 } 397 398 private WeakReference<WorkDirectory> workDirRef; 399 private String name; 400 private File historyFile; 401 private long historyFileLastModified; 402 private Vector<File> entries; 403 404 /** 405 * The name of the client property used to access the File that identifies 406 * which dynamically added menu entry has been selected. 407 * @see Listener 408 */ 409 public static final String FILE = "file"; 410 411 private static WeakHashMap<WorkDirectory, Map<String, FileHistory>> cache; 412 private static final String FILE_HISTORY = "fileHistory"; 413 private static I18NResourceBundle i18n = I18NResourceBundle.getBundleForClass(FileHistory.class); 414 415 }