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