1 /*
   2  * Copyright (c) 2011, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package java.util.prefs;
  27 
  28 import java.util.HashMap;
  29 import java.util.HashSet;
  30 import java.util.Iterator;
  31 import java.util.Timer;
  32 import java.util.TimerTask;
  33 import java.lang.ref.WeakReference;
  34 
  35 
  36 /*
  37   MacOSXPreferencesFile synchronization:
  38 
  39   Everything is synchronized on MacOSXPreferencesFile.class. This prevents:
  40   * simultaneous updates to cachedFiles or changedFiles
  41   * simultaneous creation of two objects for the same name+user+host triplet
  42   * simultaneous modifications to the same file
  43   * modifications during syncWorld/flushWorld
  44   * (in MacOSXPreferences.removeNodeSpi()) modification or sync during
  45     multi-step node removal process
  46   ... among other things.
  47 */
  48 /*
  49   Timers. There are two timers that control synchronization of prefs data to
  50   and from disk.
  51 
  52   * Sync timer periodically calls syncWorld() to force external disk changes
  53       (e.g. from another VM) into the memory cache. The sync timer runs even
  54       if there are no outstanding local changes. The sync timer syncs all live
  55       MacOSXPreferencesFile objects (the cachedFiles list).
  56     The sync timer period is controlled by the java.util.prefs.syncInterval
  57       property (same as FileSystemPreferences). By default there is *no*
  58       sync timer (unlike FileSystemPreferences); it is only enabled if the
  59       syncInterval property is set. The minimum interval is 5 seconds.
  60 
  61   * Flush timer calls flushWorld() to force local changes to disk.
  62       The flush timer is scheduled to fire some time after each pref change,
  63       unless it's already scheduled to fire before that. syncWorld and
  64       flushWorld will cancel any outstanding flush timer as unnecessary.
  65       The flush timer flushes all changed files (the changedFiles list).
  66     The time between pref write and flush timer call is controlled by the
  67       java.util.prefs.flushDelay property (unlike FileSystemPreferences).
  68       The default is 60 seconds and the minimum is 5 seconds.
  69 
  70   The flush timer's behavior is required by the Java Preferences spec
  71   ("changes will eventually propagate to the persistent backing store with
  72   an implementation-dependent delay"). The sync timer is not required by
  73   the spec (multiple VMs are only required to not corrupt the prefs), but
  74   the periodic sync is implemented by FileSystemPreferences and may be
  75   useful to some programs. The sync timer is disabled by default because
  76   it's expensive and is usually not necessary.
  77 */
  78 
  79 class MacOSXPreferencesFile {
  80 
  81     static {
  82         java.security.AccessController.doPrivileged(new sun.security.action.LoadLibraryAction("osx"));
  83     }
  84 
  85     private class FlushTask extends TimerTask {
  86         public void run() {
  87             MacOSXPreferencesFile.flushWorld();
  88         }
  89     }
  90 
  91     private class SyncTask extends TimerTask {
  92         public void run() {
  93             MacOSXPreferencesFile.syncWorld();
  94         }
  95     }
  96 
  97     // Maps string -> weak reference to MacOSXPreferencesFile
  98     private static HashMap cachedFiles = null;
  99     // Files that may have unflushed changes
 100     private static HashSet changedFiles = null;
 101 
 102 
 103     // Timer and pending sync and flush tasks (which are both scheduled
 104     // on the same timer)
 105     private static Timer timer = null;
 106     private static FlushTask flushTimerTask = null;
 107     private static long flushDelay = -1; // in seconds (min 5, default 60)
 108     private static long syncInterval = -1; // (min 5, default negative == off)
 109 
 110     private String appName;
 111     private long user;
 112     private long host;
 113 
 114     String name() { return appName; }
 115     long user() { return user; }
 116     long host() { return host; }
 117 
 118     // private contructor - use factory method getFile() instead
 119     private MacOSXPreferencesFile(String newName, long newUser, long newHost)
 120     {
 121         appName = newName;
 122         user = newUser;
 123         host = newHost;
 124     }
 125 
 126     // Factory method
 127     // Always returns the same object for the given name+user+host
 128     static synchronized MacOSXPreferencesFile
 129         getFile(String newName, boolean isUser)
 130     {
 131         MacOSXPreferencesFile result = null;
 132 
 133         if (cachedFiles == null) cachedFiles = new HashMap();
 134 
 135         String hashkey =
 136             newName + String.valueOf(isUser);
 137         WeakReference hashvalue = (WeakReference)cachedFiles.get(hashkey);
 138         if (hashvalue != null) {
 139             result = (MacOSXPreferencesFile)hashvalue.get();
 140         }
 141         if (result == null) {
 142             // Java user node == CF current user, any host
 143             // Java system node == CF any user, current host
 144             result = new MacOSXPreferencesFile(newName,
 145                                          isUser ? cfCurrentUser : cfAnyUser,
 146                                          isUser ? cfAnyHost : cfCurrentHost);
 147             cachedFiles.put(hashkey, new WeakReference(result));
 148         }
 149 
 150         // Don't schedule this file for flushing until some nodes or
 151         // keys are added to it.
 152 
 153         // Do set up the sync timer if requested; sync timer affects reads
 154         // as well as writes.
 155         initSyncTimerIfNeeded();
 156 
 157         return result;
 158     }
 159 
 160 
 161     // Write all prefs changes to disk and clear all cached prefs values
 162     // (so the next read will read from disk).
 163     static synchronized boolean syncWorld()
 164     {
 165         boolean ok = true;
 166 
 167         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 168             Iterator iter = cachedFiles.values().iterator();
 169             while (iter.hasNext()) {
 170                 WeakReference ref = (WeakReference)iter.next();
 171                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)ref.get();
 172                 if (f != null) {
 173                     if (!f.synchronize()) ok = false;
 174                 } else {
 175                     iter.remove();
 176                 }
 177             }
 178         }
 179 
 180         // Kill any pending flush
 181         if (flushTimerTask != null) {
 182             flushTimerTask.cancel();
 183             flushTimerTask = null;
 184         }
 185 
 186         // Clear changed file list. The changed files were guaranteed to
 187         // have been in the cached file list (because there was a strong
 188         // reference from changedFiles.
 189         if (changedFiles != null) changedFiles.clear();
 190 
 191         return ok;
 192     }
 193 
 194 
 195     // Write all prefs changes to disk, but do not clear all cached prefs
 196     // values. Also kills any scheduled flush task.
 197     // There's no CFPreferencesFlush() (<rdar://problem/3049129>), so lots of cached prefs
 198     // are cleared anyway.
 199     static synchronized boolean flushWorld()
 200     {
 201         boolean ok = true;
 202 
 203         if (changedFiles != null  &&  !changedFiles.isEmpty()) {
 204             Iterator iter = changedFiles.iterator();
 205             while (iter.hasNext()) {
 206                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)iter.next();
 207                 if (!f.synchronize()) ok = false;
 208             }
 209 
 210             changedFiles.clear();
 211         }
 212 
 213         if (flushTimerTask != null) {
 214             flushTimerTask.cancel();
 215             flushTimerTask = null;
 216         }
 217 
 218         return ok;
 219     }
 220 
 221     // Mark this prefs file as changed. The changes will be flushed in
 222     // at most flushDelay() seconds.
 223     // Must be called when synchronized on MacOSXPreferencesFile.class
 224     private void markChanged()
 225     {
 226         // Add this file to the changed file list
 227         if (changedFiles == null) changedFiles = new HashSet();
 228         changedFiles.add(this);
 229 
 230         // Schedule a new flush and a shutdown hook, if necessary
 231         if (flushTimerTask == null) {
 232             flushTimerTask = new FlushTask();
 233             timer().schedule(flushTimerTask, flushDelay() * 1000);
 234         }
 235     }
 236 
 237     // Return the flush delay, initializing from a property if necessary.
 238     private static synchronized long flushDelay()
 239     {
 240         if (flushDelay == -1) {
 241             try {
 242                 // flush delay >= 5, default 60
 243                 flushDelay = Math.max(5, Integer.parseInt(System.getProperty("java.util.prefs.flushDelay", "60")));
 244             } catch (NumberFormatException e) {
 245                 flushDelay = 60;
 246             }
 247         }
 248         return flushDelay;
 249     }
 250 
 251     // Initialize and run the sync timer, if the sync timer property is set
 252     // and the sync timer hasn't already been started.
 253     private static synchronized void initSyncTimerIfNeeded()
 254     {
 255         // syncInterval: -1 is uninitialized, other negative is off,
 256         // positive is seconds between syncs (min 5).
 257 
 258         if (syncInterval == -1) {
 259             try {
 260                 syncInterval = Integer.parseInt(System.getProperty("java.util.prefs.syncInterval", "-2"));
 261                 if (syncInterval >= 0) {
 262                     // minimum of 5 seconds
 263                     syncInterval = Math.max(5, syncInterval);
 264                 } else {
 265                     syncInterval = -2; // default off
 266                 }
 267             } catch (NumberFormatException e) {
 268                 syncInterval = -2; // bad property value - default off
 269             }
 270 
 271             if (syncInterval > 0) {
 272                 timer().schedule(new TimerTask() {
 273                         public void run() { MacOSXPreferencesFile.syncWorld();}
 274                     }, syncInterval * 1000, syncInterval * 1000);
 275             } else {
 276                 // syncInterval property not set. No sync timer ever.
 277             }
 278         }
 279     }
 280 
 281     // Return the timer used for flush and sync, creating it if necessary.
 282     private static synchronized Timer timer()
 283     {
 284         if (timer == null) {
 285             timer = new Timer(true); // daemon
 286             Thread flushThread = new Thread() {
 287                 public void run() {
 288                     flushWorld();
 289                 }
 290             };
 291             /* Set context class loader to null in order to avoid
 292              * keeping a strong reference to an application classloader.
 293              */
 294             flushThread.setContextClassLoader(null);
 295             Runtime.getRuntime().addShutdownHook(flushThread);
 296         }
 297         return timer;
 298     }
 299 
 300 
 301     // Node manipulation
 302     boolean addNode(String path)
 303     {
 304         synchronized(MacOSXPreferencesFile.class) {
 305             markChanged();
 306             return addNode(path, appName, user, host);
 307         }
 308     }
 309 
 310     void removeNode(String path)
 311     {
 312         synchronized(MacOSXPreferencesFile.class) {
 313             markChanged();
 314             removeNode(path, appName, user, host);
 315         }
 316     }
 317 
 318     void addChildToNode(String path, String child)
 319     {
 320         synchronized(MacOSXPreferencesFile.class) {
 321             markChanged();
 322             addChildToNode(path, child+"/", appName, user, host);
 323         }
 324     }
 325 
 326     void removeChildFromNode(String path, String child)
 327     {
 328         synchronized(MacOSXPreferencesFile.class) {
 329             markChanged();
 330             removeChildFromNode(path, child+"/", appName, user, host);
 331         }
 332     }
 333 
 334 
 335     // Key manipulation
 336     void addKeyToNode(String path, String key, String value)
 337     {
 338         synchronized(MacOSXPreferencesFile.class) {
 339             markChanged();
 340             addKeyToNode(path, key, value, appName, user, host);
 341         }
 342     }
 343 
 344     void removeKeyFromNode(String path, String key)
 345     {
 346         synchronized(MacOSXPreferencesFile.class) {
 347             markChanged();
 348             removeKeyFromNode(path, key, appName, user, host);
 349         }
 350     }
 351 
 352     String getKeyFromNode(String path, String key)
 353     {
 354         synchronized(MacOSXPreferencesFile.class) {
 355             return getKeyFromNode(path, key, appName, user, host);
 356         }
 357     }
 358 
 359 
 360     // Enumerators
 361     String[] getChildrenForNode(String path)
 362     {
 363         synchronized(MacOSXPreferencesFile.class) {
 364             return getChildrenForNode(path, appName, user, host);
 365         }
 366     }
 367 
 368     String[] getKeysForNode(String path)
 369     {
 370         synchronized(MacOSXPreferencesFile.class) {
 371             return getKeysForNode(path, appName, user, host);
 372         }
 373     }
 374 
 375 
 376     // Synchronization
 377     boolean synchronize()
 378     {
 379         synchronized(MacOSXPreferencesFile.class) {
 380             return synchronize(appName, user, host);
 381         }
 382     }
 383 
 384 
 385     // CF functions
 386     // Must be called when synchronized on MacOSXPreferencesFile.class
 387     private static final native boolean
 388         addNode(String path, String name, long user, long host);
 389     private static final native void
 390         removeNode(String path, String name, long user, long host);
 391     private static final native void
 392         addChildToNode(String path, String child,
 393                        String name, long user, long host);
 394     private static final native void
 395         removeChildFromNode(String path, String child,
 396                             String name, long user, long host);
 397     private static final native void
 398         addKeyToNode(String path, String key, String value,
 399                      String name, long user, long host);
 400     private static final native void
 401         removeKeyFromNode(String path, String key,
 402                           String name, long user, long host);
 403     private static final native String
 404         getKeyFromNode(String path, String key,
 405                        String name, long user, long host);
 406     private static final native String[]
 407         getChildrenForNode(String path, String name, long user, long host);
 408     private static final native String[]
 409         getKeysForNode(String path, String name, long user, long host);
 410     private static final native boolean
 411         synchronize(String name, long user, long host);
 412 
 413     // CFPreferences host and user values (CFStringRefs)
 414     private static long cfCurrentUser = currentUser();
 415     private static long cfAnyUser = anyUser();
 416     private static long cfCurrentHost = currentHost();
 417     private static long cfAnyHost = anyHost();
 418 
 419     // CFPreferences constant accessors
 420     private static final native long currentUser();
 421     private static final native long anyUser();
 422     private static final native long currentHost();
 423     private static final native long anyHost();
 424 }
 425