1 /*
   2  * Copyright (c) 2011, 2016, 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 =
 347                 new Thread(null, null, "Flush Thread", 0, false) {
 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