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     // Sync only current user preferences
 196     static synchronized boolean syncUser() {
 197         boolean ok = true;
 198         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 199             Iterator<WeakReference> iter = cachedFiles.values().iterator();
 200             while (iter.hasNext()) {
 201                 WeakReference ref = iter.next();
 202                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)ref.get();
 203                 if (f != null && f.user == cfCurrentUser) {
 204                     if (!f.synchronize()) { 
 205                         ok = false;
 206                     }
 207                 } else {
 208                     iter.remove();
 209                 }
 210             }
 211         }
 212         // Renove synchronized file from changed file list. The changed files were 
 213         // guaranteed to have been in the cached file list (because there was a strong
 214         // reference from changedFiles.
 215         if (changedFiles != null) {
 216             Iterator<MacOSXPreferencesFile> iterChanged = changedFiles.iterator();
 217             while (iterChanged.hasNext()) {
 218                 MacOSXPreferencesFile f = iterChanged.next();
 219                 if (f != null && f.user == cfCurrentUser) 
 220                     iterChanged.remove();
 221              }
 222         }
 223         return ok;
 224     }
 225 
 226 
 227 
 228 
 229     // Write all prefs changes to disk, but do not clear all cached prefs
 230     // values. Also kills any scheduled flush task.
 231     // There's no CFPreferencesFlush() (<rdar://problem/3049129>), so lots of cached prefs
 232     // are cleared anyway.
 233     static synchronized boolean flushWorld()
 234     {
 235         boolean ok = true;
 236 
 237         if (changedFiles != null  &&  !changedFiles.isEmpty()) {
 238             Iterator iter = changedFiles.iterator();
 239             while (iter.hasNext()) {
 240                 MacOSXPreferencesFile f = (MacOSXPreferencesFile)iter.next();
 241                 if (!f.synchronize()) ok = false;
 242             }
 243 
 244             changedFiles.clear();
 245         }
 246 
 247         if (flushTimerTask != null) {
 248             flushTimerTask.cancel();
 249             flushTimerTask = null;
 250         }
 251 
 252         return ok;
 253     }
 254 
 255     // Mark this prefs file as changed. The changes will be flushed in
 256     // at most flushDelay() seconds.
 257     // Must be called when synchronized on MacOSXPreferencesFile.class
 258     private void markChanged()
 259     {
 260         // Add this file to the changed file list
 261         if (changedFiles == null) changedFiles = new HashSet();
 262         changedFiles.add(this);
 263 
 264         // Schedule a new flush and a shutdown hook, if necessary
 265         if (flushTimerTask == null) {
 266             flushTimerTask = new FlushTask();
 267             timer().schedule(flushTimerTask, flushDelay() * 1000);
 268         }
 269     }
 270 
 271     // Return the flush delay, initializing from a property if necessary.
 272     private static synchronized long flushDelay()
 273     {
 274         if (flushDelay == -1) {
 275             try {
 276                 // flush delay >= 5, default 60
 277                 flushDelay = Math.max(5, Integer.parseInt(System.getProperty("java.util.prefs.flushDelay", "60")));
 278             } catch (NumberFormatException e) {
 279                 flushDelay = 60;
 280             }
 281         }
 282         return flushDelay;
 283     }
 284 
 285     // Initialize and run the sync timer, if the sync timer property is set
 286     // and the sync timer hasn't already been started.
 287     private static synchronized void initSyncTimerIfNeeded()
 288     {
 289         // syncInterval: -1 is uninitialized, other negative is off,
 290         // positive is seconds between syncs (min 5).
 291 
 292         if (syncInterval == -1) {
 293             try {
 294                 syncInterval = Integer.parseInt(System.getProperty("java.util.prefs.syncInterval", "-2"));
 295                 if (syncInterval >= 0) {
 296                     // minimum of 5 seconds
 297                     syncInterval = Math.max(5, syncInterval);
 298                 } else {
 299                     syncInterval = -2; // default off
 300                 }
 301             } catch (NumberFormatException e) {
 302                 syncInterval = -2; // bad property value - default off
 303             }
 304 
 305             if (syncInterval > 0) {
 306                 timer().schedule(new TimerTask() {
 307                         public void run() { MacOSXPreferencesFile.syncWorld();}
 308                     }, syncInterval * 1000, syncInterval * 1000);
 309             } else {
 310                 // syncInterval property not set. No sync timer ever.
 311             }
 312         }
 313     }
 314 
 315     // Return the timer used for flush and sync, creating it if necessary.
 316     private static synchronized Timer timer()
 317     {
 318         if (timer == null) {
 319             timer = new Timer(true); // daemon
 320             Thread flushThread = new Thread() {
 321                 public void run() {
 322                     flushWorld();
 323                 }
 324             };
 325             /* Set context class loader to null in order to avoid
 326              * keeping a strong reference to an application classloader.
 327              */
 328             flushThread.setContextClassLoader(null);
 329             Runtime.getRuntime().addShutdownHook(flushThread);
 330         }
 331         return timer;
 332     }
 333 
 334 
 335     // Node manipulation
 336     boolean addNode(String path)
 337     {
 338         synchronized(MacOSXPreferencesFile.class) {
 339             markChanged();
 340             return addNode(path, appName, user, host);
 341         }
 342     }
 343 
 344     void removeNode(String path)
 345     {
 346         synchronized(MacOSXPreferencesFile.class) {
 347             markChanged();
 348             removeNode(path, appName, user, host);
 349         }
 350     }
 351 
 352     void addChildToNode(String path, String child)
 353     {
 354         synchronized(MacOSXPreferencesFile.class) {
 355             markChanged();
 356             addChildToNode(path, child+"/", appName, user, host);
 357         }
 358     }
 359 
 360     void removeChildFromNode(String path, String child)
 361     {
 362         synchronized(MacOSXPreferencesFile.class) {
 363             markChanged();
 364             removeChildFromNode(path, child+"/", appName, user, host);
 365         }
 366     }
 367 
 368 
 369     // Key manipulation
 370     void addKeyToNode(String path, String key, String value)
 371     {
 372         synchronized(MacOSXPreferencesFile.class) {
 373             markChanged();
 374             addKeyToNode(path, key, value, appName, user, host);
 375         }
 376     }
 377 
 378     void removeKeyFromNode(String path, String key)
 379     {
 380         synchronized(MacOSXPreferencesFile.class) {
 381             markChanged();
 382             removeKeyFromNode(path, key, appName, user, host);
 383         }
 384     }
 385 
 386     String getKeyFromNode(String path, String key)
 387     {
 388         synchronized(MacOSXPreferencesFile.class) {
 389             return getKeyFromNode(path, key, appName, user, host);
 390         }
 391     }
 392 
 393 
 394     // Enumerators
 395     String[] getChildrenForNode(String path)
 396     {
 397         synchronized(MacOSXPreferencesFile.class) {
 398             return getChildrenForNode(path, appName, user, host);
 399         }
 400     }
 401 
 402     String[] getKeysForNode(String path)
 403     {
 404         synchronized(MacOSXPreferencesFile.class) {
 405             return getKeysForNode(path, appName, user, host);
 406         }
 407     }
 408 
 409 
 410     // Synchronization
 411     boolean synchronize()
 412     {
 413         synchronized(MacOSXPreferencesFile.class) {
 414             return synchronize(appName, user, host);
 415         }
 416     }
 417 
 418 
 419     // CF functions
 420     // Must be called when synchronized on MacOSXPreferencesFile.class
 421     private static final native boolean
 422         addNode(String path, String name, long user, long host);
 423     private static final native void
 424         removeNode(String path, String name, long user, long host);
 425     private static final native void
 426         addChildToNode(String path, String child,
 427                        String name, long user, long host);
 428     private static final native void
 429         removeChildFromNode(String path, String child,
 430                             String name, long user, long host);
 431     private static final native void
 432         addKeyToNode(String path, String key, String value,
 433                      String name, long user, long host);
 434     private static final native void
 435         removeKeyFromNode(String path, String key,
 436                           String name, long user, long host);
 437     private static final native String
 438         getKeyFromNode(String path, String key,
 439                        String name, long user, long host);
 440     private static final native String[]
 441         getChildrenForNode(String path, String name, long user, long host);
 442     private static final native String[]
 443         getKeysForNode(String path, String name, long user, long host);
 444     private static final native boolean
 445         synchronize(String name, long user, long host);
 446 
 447     // CFPreferences host and user values (CFStringRefs)
 448     private static long cfCurrentUser = currentUser();
 449     private static long cfAnyUser = anyUser();
 450     private static long cfCurrentHost = currentHost();
 451     private static long cfAnyHost = anyHost();
 452 
 453     // CFPreferences constant accessors
 454     private static final native long currentUser();
 455     private static final native long anyUser();
 456     private static final native long currentHost();
 457     private static final native long anyHost();
 458 }
 459