1 /*
   2  * Copyright (c) 2011, 2013, 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 import sun.misc.ManagedLocalsThread;
  35 
  36 
  37 /*
  38   MacOSXPreferencesFile synchronization:
  39 
  40   Everything is synchronized on MacOSXPreferencesFile.class. This prevents:
  41   * simultaneous updates to cachedFiles or changedFiles
  42   * simultaneous creation of two objects for the same name+user+host triplet
  43   * simultaneous modifications to the same file
  44   * modifications during syncWorld/flushWorld
  45   * (in MacOSXPreferences.removeNodeSpi()) modification or sync during
  46     multi-step node removal process
  47   ... among other things.
  48 */
  49 /*
  50   Timers. There are two timers that control synchronization of prefs data to
  51   and from disk.
  52 
  53   * Sync timer periodically calls syncWorld() to force external disk changes
  54       (e.g. from another VM) into the memory cache. The sync timer runs even
  55       if there are no outstanding local changes. The sync timer syncs all live
  56       MacOSXPreferencesFile objects (the cachedFiles list).
  57     The sync timer period is controlled by the java.util.prefs.syncInterval
  58       property (same as FileSystemPreferences). By default there is *no*
  59       sync timer (unlike FileSystemPreferences); it is only enabled if the
  60       syncInterval property is set. The minimum interval is 5 seconds.
  61 
  62   * Flush timer calls flushWorld() to force local changes to disk.
  63       The flush timer is scheduled to fire some time after each pref change,
  64       unless it's already scheduled to fire before that. syncWorld and
  65       flushWorld will cancel any outstanding flush timer as unnecessary.
  66       The flush timer flushes all changed files (the changedFiles list).
  67     The time between pref write and flush timer call is controlled by the
  68       java.util.prefs.flushDelay property (unlike FileSystemPreferences).
  69       The default is 60 seconds and the minimum is 5 seconds.
  70 
  71   The flush timer's behavior is required by the Java Preferences spec
  72   ("changes will eventually propagate to the persistent backing store with
  73   an implementation-dependent delay"). The sync timer is not required by
  74   the spec (multiple VMs are only required to not corrupt the prefs), but
  75   the periodic sync is implemented by FileSystemPreferences and may be
  76   useful to some programs. The sync timer is disabled by default because
  77   it's expensive and is usually not necessary.
  78 */
  79 
  80 class MacOSXPreferencesFile {
  81 
  82     static {
  83         java.security.AccessController.doPrivileged(
  84             new java.security.PrivilegedAction<Void>() {
  85                 public Void run() {
  86                     System.loadLibrary("prefs");
  87                     return null;
  88                 }
  89             });
  90     }
  91 
  92     private class FlushTask extends TimerTask {
  93         public void run() {
  94             MacOSXPreferencesFile.flushWorld();
  95         }
  96     }
  97 
  98     private class SyncTask extends TimerTask {
  99         public void run() {
 100             MacOSXPreferencesFile.syncWorld();
 101         }
 102     }
 103 
 104     // Maps string -> weak reference to MacOSXPreferencesFile
 105     private static HashMap<String, WeakReference<MacOSXPreferencesFile>>
 106             cachedFiles;
 107     // Files that may have unflushed changes
 108     private static HashSet<MacOSXPreferencesFile> changedFiles;
 109 
 110 
 111     // Timer and pending sync and flush tasks (which are both scheduled
 112     // on the same timer)
 113     private static Timer timer = null;
 114     private static FlushTask flushTimerTask = null;
 115     private static long flushDelay = -1; // in seconds (min 5, default 60)
 116     private static long syncInterval = -1; // (min 5, default negative == off)
 117 
 118     private String appName;
 119     private long user;
 120     private long host;
 121 
 122     String name() { return appName; }
 123     long user() { return user; }
 124     long host() { return host; }
 125 
 126     // private constructor - use factory method getFile() instead
 127     private MacOSXPreferencesFile(String newName, long newUser, long newHost)
 128     {
 129         appName = newName;
 130         user = newUser;
 131         host = newHost;
 132     }
 133 
 134     // Factory method
 135     // Always returns the same object for the given name+user+host
 136     static synchronized MacOSXPreferencesFile
 137         getFile(String newName, boolean isUser)
 138     {
 139         MacOSXPreferencesFile result = null;
 140 
 141         if (cachedFiles == null)
 142             cachedFiles = new HashMap<>();
 143 
 144         String hashkey =
 145             newName + String.valueOf(isUser);
 146         WeakReference<MacOSXPreferencesFile> hashvalue = cachedFiles.get(hashkey);
 147         if (hashvalue != null) {
 148             result = hashvalue.get();
 149         }
 150         if (result == null) {
 151             // Java user node == CF current user, any host
 152             // Java system node == CF any user, current host
 153             result = new MacOSXPreferencesFile(newName,
 154                                          isUser ? cfCurrentUser : cfAnyUser,
 155                                          isUser ? cfAnyHost : cfCurrentHost);
 156             cachedFiles.put(hashkey, new WeakReference<MacOSXPreferencesFile>(result));
 157         }
 158 
 159         // Don't schedule this file for flushing until some nodes or
 160         // keys are added to it.
 161 
 162         // Do set up the sync timer if requested; sync timer affects reads
 163         // as well as writes.
 164         initSyncTimerIfNeeded();
 165 
 166         return result;
 167     }
 168 
 169 
 170     // Write all prefs changes to disk and clear all cached prefs values
 171     // (so the next read will read from disk).
 172     static synchronized boolean syncWorld()
 173     {
 174         boolean ok = true;
 175 
 176         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 177             Iterator<WeakReference<MacOSXPreferencesFile>> iter =
 178                     cachedFiles.values().iterator();
 179             while (iter.hasNext()) {
 180                 WeakReference<MacOSXPreferencesFile> ref = iter.next();
 181                 MacOSXPreferencesFile f = ref.get();
 182                 if (f != null) {
 183                     if (!f.synchronize()) ok = false;
 184                 } else {
 185                     iter.remove();
 186                 }
 187             }
 188         }
 189 
 190         // Kill any pending flush
 191         if (flushTimerTask != null) {
 192             flushTimerTask.cancel();
 193             flushTimerTask = null;
 194         }
 195 
 196         // Clear changed file list. The changed files were guaranteed to
 197         // have been in the cached file list (because there was a strong
 198         // reference from changedFiles.
 199         if (changedFiles != null) changedFiles.clear();
 200 
 201         return ok;
 202     }
 203 
 204 
 205     // Sync only current user preferences
 206     static synchronized boolean syncUser() {
 207         boolean ok = true;
 208         if (cachedFiles != null  &&  !cachedFiles.isEmpty()) {
 209             Iterator<WeakReference<MacOSXPreferencesFile>> iter =
 210                     cachedFiles.values().iterator();
 211             while (iter.hasNext()) {
 212                 WeakReference<MacOSXPreferencesFile> ref = iter.next();
 213                 MacOSXPreferencesFile f = ref.get();
 214                 if (f != null && f.user == cfCurrentUser) {
 215                     if (!f.synchronize()) {
 216                         ok = false;
 217                     }
 218                 } else {
 219                     iter.remove();
 220                 }
 221             }
 222         }
 223         // Remove synchronized file from changed file list. The changed files were
 224         // guaranteed to have been in the cached file list (because there was a strong
 225         // reference from changedFiles.
 226         if (changedFiles != null) {
 227             Iterator<MacOSXPreferencesFile> iterChanged = changedFiles.iterator();
 228             while (iterChanged.hasNext()) {
 229                 MacOSXPreferencesFile f = iterChanged.next();
 230                 if (f != null && f.user == cfCurrentUser)
 231                     iterChanged.remove();
 232              }
 233         }
 234         return ok;
 235     }
 236 
 237     //Flush only current user preferences
 238     static synchronized boolean flushUser() {
 239         boolean ok = true;
 240         if (changedFiles != null  &&  !changedFiles.isEmpty()) {
 241             Iterator<MacOSXPreferencesFile> iterator = changedFiles.iterator();
 242             while(iterator.hasNext()) {
 243                 MacOSXPreferencesFile f = iterator.next();
 244                 if (f.user == cfCurrentUser) {
 245                     if (!f.synchronize())
 246                         ok = false;
 247                     else
 248                         iterator.remove();
 249                 }
 250             }
 251         }
 252         return ok;
 253     }
 254 
 255     // Write all prefs changes to disk, but do not clear all cached prefs
 256     // values. Also kills any scheduled flush task.
 257     // There's no CFPreferencesFlush() (<rdar://problem/3049129>), so lots of cached prefs
 258     // are cleared anyway.
 259     static synchronized boolean flushWorld()
 260     {
 261         boolean ok = true;
 262 
 263         if (changedFiles != null  &&  !changedFiles.isEmpty()) {
 264             for (MacOSXPreferencesFile f : changedFiles) {
 265                 if (!f.synchronize())
 266                     ok = false;
 267             }
 268             changedFiles.clear();
 269         }
 270 
 271         if (flushTimerTask != null) {
 272             flushTimerTask.cancel();
 273             flushTimerTask = null;
 274         }
 275 
 276         return ok;
 277     }
 278 
 279     // Mark this prefs file as changed. The changes will be flushed in
 280     // at most flushDelay() seconds.
 281     // Must be called when synchronized on MacOSXPreferencesFile.class
 282     private void markChanged()
 283     {
 284         // Add this file to the changed file list
 285         if (changedFiles == null)
 286             changedFiles = new HashSet<>();
 287         changedFiles.add(this);
 288 
 289         // Schedule a new flush and a shutdown hook, if necessary
 290         if (flushTimerTask == null) {
 291             flushTimerTask = new FlushTask();
 292             timer().schedule(flushTimerTask, flushDelay() * 1000);
 293         }
 294     }
 295 
 296     // Return the flush delay, initializing from a property if necessary.
 297     private static synchronized long flushDelay()
 298     {
 299         if (flushDelay == -1) {
 300             try {
 301                 // flush delay >= 5, default 60
 302                 flushDelay = Math.max(5, Integer.parseInt(System.getProperty("java.util.prefs.flushDelay", "60")));
 303             } catch (NumberFormatException e) {
 304                 flushDelay = 60;
 305             }
 306         }
 307         return flushDelay;
 308     }
 309 
 310     // Initialize and run the sync timer, if the sync timer property is set
 311     // and the sync timer hasn't already been started.
 312     private static synchronized void initSyncTimerIfNeeded()
 313     {
 314         // syncInterval: -1 is uninitialized, other negative is off,
 315         // positive is seconds between syncs (min 5).
 316 
 317         if (syncInterval == -1) {
 318             try {
 319                 syncInterval = Integer.parseInt(System.getProperty("java.util.prefs.syncInterval", "-2"));
 320                 if (syncInterval >= 0) {
 321                     // minimum of 5 seconds
 322                     syncInterval = Math.max(5, syncInterval);
 323                 } else {
 324                     syncInterval = -2; // default off
 325                 }
 326             } catch (NumberFormatException e) {
 327                 syncInterval = -2; // bad property value - default off
 328             }
 329 
 330             if (syncInterval > 0) {
 331                 timer().schedule(new TimerTask() {
 332                     @Override
 333                     public void run() {
 334                         MacOSXPreferencesFile.syncWorld();}
 335                     }, syncInterval * 1000, syncInterval * 1000);
 336             } else {
 337                 // syncInterval property not set. No sync timer ever.
 338             }
 339         }
 340     }
 341 
 342     // Return the timer used for flush and sync, creating it if necessary.
 343     private static synchronized Timer timer()
 344     {
 345         if (timer == null) {
 346             timer = new Timer(true); // daemon
 347             Thread flushThread = new ManagedLocalsThread() {
 348                 @Override
 349                 public void run() {
 350                     flushWorld();
 351                 }
 352             };
 353             /* Set context class loader to null in order to avoid
 354              * keeping a strong reference to an application classloader.
 355              */
 356             flushThread.setContextClassLoader(null);
 357             Runtime.getRuntime().addShutdownHook(flushThread);
 358         }
 359         return timer;
 360     }
 361 
 362 
 363     // Node manipulation
 364     boolean addNode(String path)
 365     {
 366         synchronized(MacOSXPreferencesFile.class) {
 367             markChanged();
 368             return addNode(path, appName, user, host);
 369         }
 370     }
 371 
 372     void removeNode(String path)
 373     {
 374         synchronized(MacOSXPreferencesFile.class) {
 375             markChanged();
 376             removeNode(path, appName, user, host);
 377         }
 378     }
 379 
 380     boolean addChildToNode(String path, String child)
 381     {
 382         synchronized(MacOSXPreferencesFile.class) {
 383             markChanged();
 384             return addChildToNode(path, child+"/", appName, user, host);
 385         }
 386     }
 387 
 388     void removeChildFromNode(String path, String child)
 389     {
 390         synchronized(MacOSXPreferencesFile.class) {
 391             markChanged();
 392             removeChildFromNode(path, child+"/", appName, user, host);
 393         }
 394     }
 395 
 396 
 397     // Key manipulation
 398     void addKeyToNode(String path, String key, String value)
 399     {
 400         synchronized(MacOSXPreferencesFile.class) {
 401             markChanged();
 402             addKeyToNode(path, key, value, appName, user, host);
 403         }
 404     }
 405 
 406     void removeKeyFromNode(String path, String key)
 407     {
 408         synchronized(MacOSXPreferencesFile.class) {
 409             markChanged();
 410             removeKeyFromNode(path, key, appName, user, host);
 411         }
 412     }
 413 
 414     String getKeyFromNode(String path, String key)
 415     {
 416         synchronized(MacOSXPreferencesFile.class) {
 417             return getKeyFromNode(path, key, appName, user, host);
 418         }
 419     }
 420 
 421 
 422     // Enumerators
 423     String[] getChildrenForNode(String path)
 424     {
 425         synchronized(MacOSXPreferencesFile.class) {
 426             return getChildrenForNode(path, appName, user, host);
 427         }
 428     }
 429 
 430     String[] getKeysForNode(String path)
 431     {
 432         synchronized(MacOSXPreferencesFile.class) {
 433             return getKeysForNode(path, appName, user, host);
 434         }
 435     }
 436 
 437 
 438     // Synchronization
 439     boolean synchronize()
 440     {
 441         synchronized(MacOSXPreferencesFile.class) {
 442             return synchronize(appName, user, host);
 443         }
 444     }
 445 
 446 
 447     // CF functions
 448     // Must be called when synchronized on MacOSXPreferencesFile.class
 449     private static final native boolean
 450         addNode(String path, String name, long user, long host);
 451     private static final native void
 452         removeNode(String path, String name, long user, long host);
 453     private static final native boolean
 454         addChildToNode(String path, String child,
 455                        String name, long user, long host);
 456     private static final native void
 457         removeChildFromNode(String path, String child,
 458                             String name, long user, long host);
 459     private static final native void
 460         addKeyToNode(String path, String key, String value,
 461                      String name, long user, long host);
 462     private static final native void
 463         removeKeyFromNode(String path, String key,
 464                           String name, long user, long host);
 465     private static final native String
 466         getKeyFromNode(String path, String key,
 467                        String name, long user, long host);
 468     private static final native String[]
 469         getChildrenForNode(String path, String name, long user, long host);
 470     private static final native String[]
 471         getKeysForNode(String path, String name, long user, long host);
 472     private static final native boolean
 473         synchronize(String name, long user, long host);
 474 
 475     // CFPreferences host and user values (CFStringRefs)
 476     private static long cfCurrentUser = currentUser();
 477     private static long cfAnyUser = anyUser();
 478     private static long cfCurrentHost = currentHost();
 479     private static long cfAnyHost = anyHost();
 480 
 481     // CFPreferences constant accessors
 482     private static final native long currentUser();
 483     private static final native long anyUser();
 484     private static final native long currentHost();
 485     private static final native long anyHost();
 486 }
 487