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 }