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(
  83             new java.security.PrivilegedAction<Void>() {
  84                 public Void run() {
  85                     System.loadLibrary("osx");
  86                     return null;
  87                 }
  88             });
  89     }
  90 
  91     private class FlushTask extends TimerTask {
  92         public void run() {
  93             MacOSXPreferencesFile.flushWorld();
  94         }
  95     }
  96 
  97     private class SyncTask extends TimerTask {
  98         public void run() {
  99             MacOSXPreferencesFile.syncWorld();
 100         }
 101     }
 102 
 103     // Maps string -> weak reference to MacOSXPreferencesFile
 104     private static HashMap cachedFiles = null;
 105     // Files that may have unflushed changes
 106     private static HashSet changedFiles = null;
 107 
 108 
 109     // Timer and pending sync and flush tasks (which are both scheduled
 110     // on the same timer)
 111     private static Timer timer = null;
 112     private static FlushTask flushTimerTask = null;
 113     private static long flushDelay = -1; // in seconds (min 5, default 60)
 114     private static long syncInterval = -1; // (min 5, default negative == off)
 115 
 116     private String appName;
 117     private long user;
 118     private long host;
 119 
 120     String name() { return appName; }
 121     long user() { return user; }
 122     long host() { return host; }
 123 
 124     // private contructor - use factory method getFile() instead
 125     private MacOSXPreferencesFile(String newName, long newUser, long newHost)
 126     {
 127         appName = newName;
 128         user = newUser;
 129         host = newHost;
 130     }
 131 
 132     // Factory method
 133     // Always returns the same object for the given name+user+host
 134     static synchronized MacOSXPreferencesFile
 135         getFile(String newName, boolean isUser)
 136     {
 137         MacOSXPreferencesFile result = null;
 138 
 139         if (cachedFiles == null) cachedFiles = new HashMap();
 140 
 141         String hashkey =
 142             newName + String.valueOf(isUser);
 143         WeakReference hashvalue = (WeakReference)cachedFiles.get(hashkey);
 144         if (hashvalue != null) {
 145             result = (MacOSXPreferencesFile)hashvalue.get();
 146         }
 147         if (result == null) {
 148             // Java user node == CF current user, any host
 149             // Java system node == CF any user, current host
 150             result = new MacOSXPreferencesFile(newName,
 151                                          isUser ? cfCurrentUser : cfAnyUser,
 152                                          isUser ? cfAnyHost : cfCurrentHost);
 153             cachedFiles.put(hashkey, new WeakReference(result));
 154         }
 155 
 156         // Don't schedule this file for flushing until some nodes or
 157         // keys are added to it.
 158 
 159         // Do set up the sync timer if requested; sync timer affects reads
 160         // as well as writes.
 161         initSyncTimerIfNeeded();
 162 
 163         return result;
 164     }
 165 
 166 
 167     // Write all prefs changes to disk and clear all cached prefs values
 168     // (so the next read will read from disk).
 169     static synchronized boolean syncWorld()
 170     {
 171         boolean ok = true;
 172 
 173         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 174             Iterator iter = cachedFiles.values().iterator();
 175             while (iter.hasNext()) {
 176                 WeakReference ref = (WeakReference)iter.next();
 177                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)ref.get();
 178                 if (f != null) {
 179                     if (!f.synchronize()) ok = false;
 180                 } else {
 181                     iter.remove();
 182                 }
 183             }
 184         }
 185 
 186         // Kill any pending flush
 187         if (flushTimerTask != null) {
 188             flushTimerTask.cancel();
 189             flushTimerTask = null;
 190         }
 191 
 192         // Clear changed file list. The changed files were guaranteed to
 193         // have been in the cached file list (because there was a strong
 194         // reference from changedFiles.
 195         if (changedFiles != null) changedFiles.clear();
 196 
 197         return ok;
 198     }
 199 
 200 
 201     // Write all prefs changes to disk, but do not clear all cached prefs
 202     // values. Also kills any scheduled flush task.
 203     // There's no CFPreferencesFlush() (<rdar://problem/3049129>), so lots of cached prefs
 204     // are cleared anyway.
 205     static synchronized boolean flushWorld()
 206     {
 207         boolean ok = true;
 208 
 209         if (changedFiles != null  &&  !changedFiles.isEmpty()) {
 210             Iterator iter = changedFiles.iterator();
 211             while (iter.hasNext()) {
 212                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)iter.next();
 213                 if (!f.synchronize()) ok = false;
 214             }
 215 
 216             changedFiles.clear();
 217         }
 218 
 219         if (flushTimerTask != null) {
 220             flushTimerTask.cancel();
 221             flushTimerTask = null;
 222         }
 223 
 224         return ok;
 225     }
 226 
 227     // Mark this prefs file as changed. The changes will be flushed in
 228     // at most flushDelay() seconds.
 229     // Must be called when synchronized on MacOSXPreferencesFile.class
 230     private void markChanged()
 231     {
 232         // Add this file to the changed file list
 233         if (changedFiles == null) changedFiles = new HashSet();
 234         changedFiles.add(this);
 235 
 236         // Schedule a new flush and a shutdown hook, if necessary
 237         if (flushTimerTask == null) {
 238             flushTimerTask = new FlushTask();
 239             timer().schedule(flushTimerTask, flushDelay() * 1000);
 240         }
 241     }
 242 
 243     // Return the flush delay, initializing from a property if necessary.
 244     private static synchronized long flushDelay()
 245     {
 246         if (flushDelay == -1) {
 247             try {
 248                 // flush delay >= 5, default 60
 249                 flushDelay = Math.max(5, Integer.parseInt(System.getProperty("java.util.prefs.flushDelay", "60")));
 250             } catch (NumberFormatException e) {
 251                 flushDelay = 60;
 252             }
 253         }
 254         return flushDelay;
 255     }
 256 
 257     // Initialize and run the sync timer, if the sync timer property is set
 258     // and the sync timer hasn't already been started.
 259     private static synchronized void initSyncTimerIfNeeded()
 260     {
 261         // syncInterval: -1 is uninitialized, other negative is off,
 262         // positive is seconds between syncs (min 5).
 263 
 264         if (syncInterval == -1) {
 265             try {
 266                 syncInterval = Integer.parseInt(System.getProperty("java.util.prefs.syncInterval", "-2"));
 267                 if (syncInterval >= 0) {
 268                     // minimum of 5 seconds
 269                     syncInterval = Math.max(5, syncInterval);
 270                 } else {
 271                     syncInterval = -2; // default off
 272                 }
 273             } catch (NumberFormatException e) {
 274                 syncInterval = -2; // bad property value - default off
 275             }
 276 
 277             if (syncInterval > 0) {
 278                 timer().schedule(new TimerTask() {
 279                         public void run() { MacOSXPreferencesFile.syncWorld();}
 280                     }, syncInterval * 1000, syncInterval * 1000);
 281             } else {
 282                 // syncInterval property not set. No sync timer ever.
 283             }
 284         }
 285     }
 286 
 287     // Return the timer used for flush and sync, creating it if necessary.
 288     private static synchronized Timer timer()
 289     {
 290         if (timer == null) {
 291             timer = new Timer(true); // daemon
 292             Thread flushThread = new Thread() {
 293                 public void run() {
 294                     flushWorld();
 295                 }
 296             };
 297             /* Set context class loader to null in order to avoid
 298              * keeping a strong reference to an application classloader.
 299              */
 300             flushThread.setContextClassLoader(null);
 301             Runtime.getRuntime().addShutdownHook(flushThread);
 302         }
 303         return timer;
 304     }
 305 
 306 
 307     // Node manipulation
 308     boolean addNode(String path)
 309     {
 310         synchronized(MacOSXPreferencesFile.class) {
 311             markChanged();
 312             return addNode(path, appName, user, host);
 313         }
 314     }
 315 
 316     void removeNode(String path)
 317     {
 318         synchronized(MacOSXPreferencesFile.class) {
 319             markChanged();
 320             removeNode(path, appName, user, host);
 321         }
 322     }
 323 
 324     void addChildToNode(String path, String child)
 325     {
 326         synchronized(MacOSXPreferencesFile.class) {
 327             markChanged();
 328             addChildToNode(path, child+"/", appName, user, host);
 329         }
 330     }
 331 
 332     void removeChildFromNode(String path, String child)
 333     {
 334         synchronized(MacOSXPreferencesFile.class) {
 335             markChanged();
 336             removeChildFromNode(path, child+"/", appName, user, host);
 337         }
 338     }
 339 
 340 
 341     // Key manipulation
 342     void addKeyToNode(String path, String key, String value)
 343     {
 344         synchronized(MacOSXPreferencesFile.class) {
 345             markChanged();
 346             addKeyToNode(path, key, value, appName, user, host);
 347         }
 348     }
 349 
 350     void removeKeyFromNode(String path, String key)
 351     {
 352         synchronized(MacOSXPreferencesFile.class) {
 353             markChanged();
 354             removeKeyFromNode(path, key, appName, user, host);
 355         }
 356     }
 357 
 358     String getKeyFromNode(String path, String key)
 359     {
 360         synchronized(MacOSXPreferencesFile.class) {
 361             return getKeyFromNode(path, key, appName, user, host);
 362         }
 363     }
 364 
 365 
 366     // Enumerators
 367     String[] getChildrenForNode(String path)
 368     {
 369         synchronized(MacOSXPreferencesFile.class) {
 370             return getChildrenForNode(path, appName, user, host);
 371         }
 372     }
 373 
 374     String[] getKeysForNode(String path)
 375     {
 376         synchronized(MacOSXPreferencesFile.class) {
 377             return getKeysForNode(path, appName, user, host);
 378         }
 379     }
 380 
 381 
 382     // Synchronization
 383     boolean synchronize()
 384     {
 385         synchronized(MacOSXPreferencesFile.class) {
 386             return synchronize(appName, user, host);
 387         }
 388     }
 389 
 390 
 391     // CF functions
 392     // Must be called when synchronized on MacOSXPreferencesFile.class
 393     private static final native boolean
 394         addNode(String path, String name, long user, long host);
 395     private static final native void
 396         removeNode(String path, String name, long user, long host);
 397     private static final native void
 398         addChildToNode(String path, String child,
 399                        String name, long user, long host);
 400     private static final native void
 401         removeChildFromNode(String path, String child,
 402                             String name, long user, long host);
 403     private static final native void
 404         addKeyToNode(String path, String key, String value,
 405                      String name, long user, long host);
 406     private static final native void
 407         removeKeyFromNode(String path, String key,
 408                           String name, long user, long host);
 409     private static final native String
 410         getKeyFromNode(String path, String key,
 411                        String name, long user, long host);
 412     private static final native String[]
 413         getChildrenForNode(String path, String name, long user, long host);
 414     private static final native String[]
 415         getKeysForNode(String path, String name, long user, long host);
 416     private static final native boolean
 417         synchronize(String name, long user, long host);
 418 
 419     // CFPreferences host and user values (CFStringRefs)
 420     private static long cfCurrentUser = currentUser();
 421     private static long cfAnyUser = anyUser();
 422     private static long cfCurrentHost = currentHost();
 423     private static long cfAnyHost = anyHost();
 424 
 425     // CFPreferences constant accessors
 426     private static final native long currentUser();
 427     private static final native long anyUser();
 428     private static final native long currentHost();
 429     private static final native long anyHost();
 430 }
 431