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