1 /*
2 * Copyright (c) 2000, 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 import java.util;
28 import java.io;
29 import java.security.AccessController;
30 import java.security.PrivilegedAction;
31 import java.security.PrivilegedExceptionAction;
32 import java.security.PrivilegedActionException;
33
34 import sun.util.logging.PlatformLogger;
35
36 /**
37 * Preferences implementation for Unix. Preferences are stored in the file
38 * system, with one directory per preferences node. All of the preferences
39 * at each node are stored in a single file. Atomic file system operations
40 * (e.g. File.renameTo) are used to ensure integrity. An in-memory cache of
41 * the "explored" portion of the tree is maintained for performance, and
42 * written back to the disk periodically. File-locking is used to ensure
43 * reasonable behavior when multiple VMs are running at the same time.
44 * (The file lock is obtained only for sync(), flush() and removeNode().)
45 *
46 * @author Josh Bloch
47 * @see Preferences
48 * @since 1.4
49 */
50 class FileSystemPreferences extends AbstractPreferences {
51 /**
52 * Sync interval in seconds.
53 */
54 private static final int SYNC_INTERVAL = Math.max(1,
55 Integer.parseInt(
56 AccessController.doPrivileged(
57 new sun.security.action.GetPropertyAction(
58 "java.util.prefs.syncInterval", "30"))));
59
60 /**
61 * Returns logger for error messages. Backing store exceptions are logged at
62 * WARNING level.
63 */
64 private static PlatformLogger getLogger() {
65 return PlatformLogger.getLogger("java.util.prefs");
66 }
67
68 /**
69 * Directory for system preferences.
70 */
71 private static File systemRootDir;
72
73 /*
74 * Flag, indicating whether systemRoot directory is writable
75 */
76 private static boolean isSystemRootWritable;
77
78 /**
79 * Directory for user preferences.
80 */
81 private static File userRootDir;
82
83 /*
84 * Flag, indicating whether userRoot directory is writable
85 */
86 private static boolean isUserRootWritable;
87
88 /**
89 * The user root.
90 */
91 static Preferences userRoot = null;
92
93 static synchronized Preferences getUserRoot() {
94 if (userRoot == null) {
95 setupUserRoot();
96 userRoot = new FileSystemPreferences(true);
97 }
98 return userRoot;
99 }
100
101 private static void setupUserRoot() {
102 AccessController.doPrivileged(new PrivilegedAction<Void>() {
103 public Void run() {
104 userRootDir =
105 new File(System.getProperty("java.util.prefs.userRoot",
106 System.getProperty("user.home")), ".java/.userPrefs");
107 // Attempt to create root dir if it does not yet exist.
108 if (!userRootDir.exists()) {
109 if (userRootDir.mkdirs()) {
110 try {
111 chmod(userRootDir.getCanonicalPath(), USER_RWX);
112 } catch (IOException e) {
113 getLogger().warning("Could not change permissions" +
114 " on userRoot directory. ");
115 }
116 getLogger().info("Created user preferences directory.");
117 }
118 else
119 getLogger().warning("Couldn't create user preferences" +
120 " directory. User preferences are unusable.");
121 }
122 isUserRootWritable = userRootDir.canWrite();
123 String USER_NAME = System.getProperty("user.name");
124 userLockFile = new File (userRootDir,".user.lock." + USER_NAME);
125 userRootModFile = new File (userRootDir,
126 ".userRootModFile." + USER_NAME);
127 if (!userRootModFile.exists())
128 try {
129 // create if does not exist.
130 userRootModFile.createNewFile();
131 // Only user can read/write userRootModFile.
132 int result = chmod(userRootModFile.getCanonicalPath(),
133 USER_READ_WRITE);
134 if (result !=0)
135 getLogger().warning("Problem creating userRoot " +
136 "mod file. Chmod failed on " +
137 userRootModFile.getCanonicalPath() +
138 " Unix error code " + result);
139 } catch (IOException e) {
140 getLogger().warning(e.toString());
141 }
142 userRootModTime = userRootModFile.lastModified();
143 return null;
144 }
145 });
146 }
147
148
149 /**
150 * The system root.
151 */
152 static Preferences systemRoot;
153
154 static synchronized Preferences getSystemRoot() {
155 if (systemRoot == null) {
156 setupSystemRoot();
157 systemRoot = new FileSystemPreferences(false);
158 }
159 return systemRoot;
160 }
161
162 private static void setupSystemRoot() {
163 AccessController.doPrivileged(new PrivilegedAction<Void>() {
164 public Void run() {
165 String systemPrefsDirName =
166 System.getProperty("java.util.prefs.systemRoot","/etc/.java");
167 systemRootDir =
168 new File(systemPrefsDirName, ".systemPrefs");
169 // Attempt to create root dir if it does not yet exist.
170 if (!systemRootDir.exists()) {
171 // system root does not exist in /etc/.java
172 // Switching to java.home
173 systemRootDir =
174 new File(System.getProperty("java.home"),
175 ".systemPrefs");
176 if (!systemRootDir.exists()) {
177 if (systemRootDir.mkdirs()) {
178 getLogger().info(
179 "Created system preferences directory "
180 + "in java.home.");
181 try {
182 chmod(systemRootDir.getCanonicalPath(),
183 USER_RWX_ALL_RX);
184 } catch (IOException e) {
185 }
186 } else {
187 getLogger().warning("Could not create "
188 + "system preferences directory. System "
189 + "preferences are unusable.");
190 }
191 }
192 }
193 isSystemRootWritable = systemRootDir.canWrite();
194 systemLockFile = new File(systemRootDir, ".system.lock");
195 systemRootModFile =
196 new File (systemRootDir,".systemRootModFile");
197 if (!systemRootModFile.exists() && isSystemRootWritable)
198 try {
199 // create if does not exist.
200 systemRootModFile.createNewFile();
201 int result = chmod(systemRootModFile.getCanonicalPath(),
202 USER_RW_ALL_READ);
203 if (result !=0)
204 getLogger().warning("Chmod failed on " +
205 systemRootModFile.getCanonicalPath() +
206 " Unix error code " + result);
207 } catch (IOException e) { getLogger().warning(e.toString());
208 }
209 systemRootModTime = systemRootModFile.lastModified();
210 return null;
211 }
212 });
213 }
214
215
216 /**
217 * Unix user write/read permission
218 */
219 private static final int USER_READ_WRITE = 0600;
220
221 private static final int USER_RW_ALL_READ = 0644;
222
223
224 private static final int USER_RWX_ALL_RX = 0755;
225
226 private static final int USER_RWX = 0700;
227
228 /**
229 * The lock file for the user tree.
230 */
231 static File userLockFile;
232
233
234
235 /**
236 * The lock file for the system tree.
237 */
238 static File systemLockFile;
239
240 /**
241 * Unix lock handle for userRoot.
242 * Zero, if unlocked.
243 */
244
245 private static int userRootLockHandle = 0;
246
247 /**
248 * Unix lock handle for systemRoot.
249 * Zero, if unlocked.
250 */
251
252 private static int systemRootLockHandle = 0;
253
254 /**
255 * The directory representing this preference node. There is no guarantee
256 * that this directory exits, as another VM can delete it at any time
257 * that it (the other VM) holds the file-lock. While the root node cannot
258 * be deleted, it may not yet have been created, or the underlying
259 * directory could have been deleted accidentally.
260 */
261 private final File dir;
262
263 /**
264 * The file representing this preference node's preferences.
265 * The file format is undocumented, and subject to change
266 * from release to release, but I'm sure that you can figure
267 * it out if you try real hard.
268 */
269 private final File prefsFile;
270
271 /**
272 * A temporary file used for saving changes to preferences. As part of
273 * the sync operation, changes are first saved into this file, and then
274 * atomically renamed to prefsFile. This results in an atomic state
275 * change from one valid set of preferences to another. The
276 * the file-lock is held for the duration of this transformation.
277 */
278 private final File tmpFile;
279
280 /**
281 * File, which keeps track of global modifications of userRoot.
282 */
283 private static File userRootModFile;
284
285 /**
286 * Flag, which indicated whether userRoot was modified by another VM
287 */
288 private static boolean isUserRootModified = false;
289
290 /**
291 * Keeps track of userRoot modification time. This time is reset to
292 * zero after UNIX reboot, and is increased by 1 second each time
293 * userRoot is modified.
294 */
295 private static long userRootModTime;
296
297
298 /*
299 * File, which keeps track of global modifications of systemRoot
300 */
301 private static File systemRootModFile;
302 /*
303 * Flag, which indicates whether systemRoot was modified by another VM
304 */
305 private static boolean isSystemRootModified = false;
306
307 /**
308 * Keeps track of systemRoot modification time. This time is reset to
309 * zero after system reboot, and is increased by 1 second each time
310 * systemRoot is modified.
311 */
312 private static long systemRootModTime;
313
314 /**
315 * Locally cached preferences for this node (includes uncommitted
316 * changes). This map is initialized with from disk when the first get or
317 * put operation occurs on this node. It is synchronized with the
318 * corresponding disk file (prefsFile) by the sync operation. The initial
319 * value is read *without* acquiring the file-lock.
320 */
321 private Map<String, String> prefsCache = null;
322
323 /**
324 * The last modification time of the file backing this node at the time
325 * that prefCache was last synchronized (or initially read). This
326 * value is set *before* reading the file, so it's conservative; the
327 * actual timestamp could be (slightly) higher. A value of zero indicates
328 * that we were unable to initialize prefsCache from the disk, or
329 * have not yet attempted to do so. (If prefsCache is non-null, it
330 * indicates the former; if it's null, the latter.)
331 */
332 private long lastSyncTime = 0;
333
334 /**
335 * Unix error code for locked file.
336 */
337 private static final int EAGAIN = 11;
338
339 /**
340 * Unix error code for denied access.
341 */
342 private static final int EACCES = 13;
343
344 /* Used to interpret results of native functions */
345 private static final int LOCK_HANDLE = 0;
346 private static final int ERROR_CODE = 1;
347
348 /**
349 * A list of all uncommitted preference changes. The elements in this
350 * list are of type PrefChange. If this node is concurrently modified on
351 * disk by another VM, the two sets of changes are merged when this node
352 * is sync'ed by overwriting our prefsCache with the preference map last
353 * written out to disk (by the other VM), and then replaying this change
354 * log against that map. The resulting map is then written back
355 * to the disk.
356 */
357 final List<Change> changeLog = new ArrayList<>();
358
359 /**
360 * Represents a change to a preference.
361 */
362 private abstract class Change {
363 /**
364 * Reapplies the change to prefsCache.
365 */
366 abstract void replay();
367 };
368
369 /**
370 * Represents a preference put.
371 */
372 private class Put extends Change {
373 String key, value;
374
375 Put(String key, String value) {
376 this.key = key;
377 this.value = value;
378 }
379
380 void replay() {
381 prefsCache.put(key, value);
382 }
383 }
384
385 /**
386 * Represents a preference remove.
387 */
388 private class Remove extends Change {
389 String key;
390
391 Remove(String key) {
392 this.key = key;
393 }
394
395 void replay() {
396 prefsCache.remove(key);
397 }
398 }
399
400 /**
401 * Represents the creation of this node.
402 */
403 private class NodeCreate extends Change {
404 /**
405 * Performs no action, but the presence of this object in changeLog
406 * will force the node and its ancestors to be made permanent at the
407 * next sync.
408 */
409 void replay() {
410 }
411 }
412
413 /**
414 * NodeCreate object for this node.
415 */
416 NodeCreate nodeCreate = null;
417
418 /**
419 * Replay changeLog against prefsCache.
420 */
421 private void replayChanges() {
422 for (int i = 0, n = changeLog.size(); i<n; i++)
423 changeLog.get(i).replay();
424 }
425
426 private static Timer syncTimer = new Timer(true); // Daemon Thread
427
428 static {
429 // Add periodic timer task to periodically sync cached prefs
430 syncTimer.schedule(new TimerTask() {
431 public void run() {
432 syncWorld();
433 }
434 }, SYNC_INTERVAL*1000, SYNC_INTERVAL*1000);
435
436 // Add shutdown hook to flush cached prefs on normal termination
437 AccessController.doPrivileged(new PrivilegedAction<Void>() {
438 public Void run() {
439 Runtime.getRuntime().addShutdownHook(new Thread() {
440 public void run() {
441 syncTimer.cancel();
442 syncWorld();
443 }
444 });
445 return null;
446 }
447 });
448 }
449
450 private static void syncWorld() {
451 /*
452 * Synchronization necessary because userRoot and systemRoot are
453 * lazily initialized.
454 */
455 Preferences userRt;
456 Preferences systemRt;
457 synchronized(FileSystemPreferences.class) {
458 userRt = userRoot;
459 systemRt = systemRoot;
460 }
461
462 try {
463 if (userRt != null)
464 userRt.flush();
465 } catch(BackingStoreException e) {
466 getLogger().warning("Couldn't flush user prefs: " + e);
467 }
468
469 try {
470 if (systemRt != null)
471 systemRt.flush();
472 } catch(BackingStoreException e) {
473 getLogger().warning("Couldn't flush system prefs: " + e);
474 }
475 }
476
477 private final boolean isUserNode;
478
479 /**
480 * Special constructor for roots (both user and system). This constructor
481 * will only be called twice, by the static initializer.
482 */
483 private FileSystemPreferences(boolean user) {
484 super(null, "");
485 isUserNode = user;
486 dir = (user ? userRootDir: systemRootDir);
487 prefsFile = new File(dir, "prefs.xml");
488 tmpFile = new File(dir, "prefs.tmp");
489 }
490
491 /**
492 * Construct a new FileSystemPreferences instance with the specified
493 * parent node and name. This constructor, called from childSpi,
494 * is used to make every node except for the two //roots.
495 */
496 private FileSystemPreferences(FileSystemPreferences parent, String name) {
497 super(parent, name);
498 isUserNode = parent.isUserNode;
499 dir = new File(parent.dir, dirName(name));
500 prefsFile = new File(dir, "prefs.xml");
501 tmpFile = new File(dir, "prefs.tmp");
502 AccessController.doPrivileged(new PrivilegedAction<Void>() {
503 public Void run() {
504 newNode = !dir.exists();
505 return null;
506 }
507 });
508 if (newNode) {
509 // These 2 things guarantee node will get wrtten at next flush/sync
510 prefsCache = new TreeMap<>();
511 nodeCreate = new NodeCreate();
512 changeLog.add(nodeCreate);
513 }
514 }
515
516 public boolean isUserNode() {
517 return isUserNode;
518 }
519
520 protected void putSpi(String key, String value) {
521 initCacheIfNecessary();
522 changeLog.add(new Put(key, value));
523 prefsCache.put(key, value);
524 }
525
526 protected String getSpi(String key) {
527 initCacheIfNecessary();
528 return prefsCache.get(key);
529 }
530
531 protected void removeSpi(String key) {
532 initCacheIfNecessary();
533 changeLog.add(new Remove(key));
534 prefsCache.remove(key);
535 }
536
537 /**
538 * Initialize prefsCache if it has yet to be initialized. When this method
539 * returns, prefsCache will be non-null. If the data was successfully
540 * read from the file, lastSyncTime will be updated. If prefsCache was
541 * null, but it was impossible to read the file (because it didn't
542 * exist or for any other reason) prefsCache will be initialized to an
543 * empty, modifiable Map, and lastSyncTime remain zero.
544 */
545 private void initCacheIfNecessary() {
546 if (prefsCache != null)
547 return;
548
549 try {
550 loadCache();
551 } catch(Exception e) {
552 // assert lastSyncTime == 0;
553 prefsCache = new TreeMap<>();
554 }
555 }
556
557 /**
558 * Attempt to load prefsCache from the backing store. If the attempt
559 * succeeds, lastSyncTime will be updated (the new value will typically
560 * correspond to the data loaded into the map, but it may be less,
561 * if another VM is updating this node concurrently). If the attempt
562 * fails, a BackingStoreException is thrown and both prefsCache and
563 * lastSyncTime are unaffected by the call.
564 */
565 private void loadCache() throws BackingStoreException {
566 try {
567 AccessController.doPrivileged(
568 new PrivilegedExceptionAction<Void>() {
569 public Void run() throws BackingStoreException {
570 Map<String, String> m = new TreeMap<>();
571 long newLastSyncTime = 0;
572 try {
573 newLastSyncTime = prefsFile.lastModified();
574 try (FileInputStream fis = new FileInputStream(prefsFile)) {
575 XmlSupport.importMap(fis, m);
576 }
577 } catch(Exception e) {
578 if (e instanceof InvalidPreferencesFormatException) {
579 getLogger().warning("Invalid preferences format in "
580 + prefsFile.getPath());
581 prefsFile.renameTo( new File(
582 prefsFile.getParentFile(),
583 "IncorrectFormatPrefs.xml"));
584 m = new TreeMap<>();
585 } else if (e instanceof FileNotFoundException) {
586 getLogger().warning("Prefs file removed in background "
587 + prefsFile.getPath());
588 } else {
589 throw new BackingStoreException(e);
590 }
591 }
592 // Attempt succeeded; update state
593 prefsCache = m;
594 lastSyncTime = newLastSyncTime;
595 return null;
596 }
597 });
598 } catch (PrivilegedActionException e) {
599 throw (BackingStoreException) e.getException();
600 }
601 }
602
603 /**
604 * Attempt to write back prefsCache to the backing store. If the attempt
605 * succeeds, lastSyncTime will be updated (the new value will correspond
606 * exactly to the data thust written back, as we hold the file lock, which
607 * prevents a concurrent write. If the attempt fails, a
608 * BackingStoreException is thrown and both the backing store (prefsFile)
609 * and lastSyncTime will be unaffected by this call. This call will
610 * NEVER leave prefsFile in a corrupt state.
611 */
612 private void writeBackCache() throws BackingStoreException {
613 try {
614 AccessController.doPrivileged(
615 new PrivilegedExceptionAction<Void>() {
616 public Void run() throws BackingStoreException {
617 try {
618 if (!dir.exists() && !dir.mkdirs())
619 throw new BackingStoreException(dir +
620 " create failed.");
621 try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
622 XmlSupport.exportMap(fos, prefsCache);
623 }
624 if (!tmpFile.renameTo(prefsFile))
625 throw new BackingStoreException("Can't rename " +
626 tmpFile + " to " + prefsFile);
627 } catch(Exception e) {
628 if (e instanceof BackingStoreException)
629 throw (BackingStoreException)e;
630 throw new BackingStoreException(e);
631 }
632 return null;
633 }
634 });
635 } catch (PrivilegedActionException e) {
636 throw (BackingStoreException) e.getException();
637 }
638 }
639
640 protected String[] keysSpi() {
641 initCacheIfNecessary();
642 return prefsCache.keySet().toArray(new String[prefsCache.size()]);
643 }
644
645 protected String[] childrenNamesSpi() {
646 return AccessController.doPrivileged(
647 new PrivilegedAction<String[]>() {
648 public String[] run() {
649 List<String> result = new ArrayList<>();
650 File[] dirContents = dir.listFiles();
651 if (dirContents != null) {
652 for (int i = 0; i < dirContents.length; i++)
653 if (dirContents[i].isDirectory())
654 result.add(nodeName(dirContents[i].getName()));
655 }
656 return result.toArray(EMPTY_STRING_ARRAY);
657 }
658 });
659 }
660
661 private static final String[] EMPTY_STRING_ARRAY = new String[0];
662
663 protected AbstractPreferences childSpi(String name) {
664 return new FileSystemPreferences(this, name);
665 }
666
667 public void removeNode() throws BackingStoreException {
668 synchronized (isUserNode()? userLockFile: systemLockFile) {
669 // to remove a node we need an exclusive lock
670 if (!lockFile(false))
671 throw(new BackingStoreException("Couldn't get file lock."));
672 try {
673 super.removeNode();
674 } finally {
675 unlockFile();
676 }
677 }
678 }
679
680 /**
681 * Called with file lock held (in addition to node locks).
682 */
683 protected void removeNodeSpi() throws BackingStoreException {
684 try {
685 AccessController.doPrivileged(
686 new PrivilegedExceptionAction<Void>() {
687 public Void run() throws BackingStoreException {
688 if (changeLog.contains(nodeCreate)) {
689 changeLog.remove(nodeCreate);
690 nodeCreate = null;
691 return null;
692 }
693 if (!dir.exists())
694 return null;
695 prefsFile.delete();
696 tmpFile.delete();
697 // dir should be empty now. If it's not, empty it
698 File[] junk = dir.listFiles();
699 if (junk.length != 0) {
700 getLogger().warning(
701 "Found extraneous files when removing node: "
702 + Arrays.asList(junk));
703 for (int i=0; i<junk.length; i++)
704 junk[i].delete();
705 }
706 if (!dir.delete())
707 throw new BackingStoreException("Couldn't delete dir: "
708 + dir);
709 return null;
710 }
711 });
712 } catch (PrivilegedActionException e) {
713 throw (BackingStoreException) e.getException();
714 }
715 }
716
717 public synchronized void sync() throws BackingStoreException {
718 boolean userNode = isUserNode();
719 boolean shared;
720
721 if (userNode) {
722 shared = false; /* use exclusive lock for user prefs */
723 } else {
724 /* if can write to system root, use exclusive lock.
725 otherwise use shared lock. */
726 shared = !isSystemRootWritable;
727 }
728 synchronized (isUserNode()? userLockFile:systemLockFile) {
729 if (!lockFile(shared))
730 throw(new BackingStoreException("Couldn't get file lock."));
731 final Long newModTime =
732 AccessController.doPrivileged(
733 new PrivilegedAction<Long>() {
734 public Long run() {
735 long nmt;
736 if (isUserNode()) {
737 nmt = userRootModFile.lastModified();
738 isUserRootModified = userRootModTime == nmt;
739 } else {
740 nmt = systemRootModFile.lastModified();
741 isSystemRootModified = systemRootModTime == nmt;
742 }
743 return new Long(nmt);
744 }
745 });
746 try {
747 super.sync();
748 AccessController.doPrivileged(new PrivilegedAction<Void>() {
749 public Void run() {
750 if (isUserNode()) {
751 userRootModTime = newModTime.longValue() + 1000;
752 userRootModFile.setLastModified(userRootModTime);
753 } else {
754 systemRootModTime = newModTime.longValue() + 1000;
755 systemRootModFile.setLastModified(systemRootModTime);
756 }
757 return null;
758 }
759 });
760 } finally {
761 unlockFile();
762 }
763 }
764 }
765
766 protected void syncSpi() throws BackingStoreException {
767 try {
768 AccessController.doPrivileged(
769 new PrivilegedExceptionAction<Void>() {
770 public Void run() throws BackingStoreException {
771 syncSpiPrivileged();
772 return null;
773 }
774 });
775 } catch (PrivilegedActionException e) {
776 throw (BackingStoreException) e.getException();
777 }
778 }
779 private void syncSpiPrivileged() throws BackingStoreException {
780 if (isRemoved())
781 throw new IllegalStateException("Node has been removed");
782 if (prefsCache == null)
783 return; // We've never been used, don't bother syncing
784 long lastModifiedTime;
785 if ((isUserNode() ? isUserRootModified : isSystemRootModified)) {
786 lastModifiedTime = prefsFile.lastModified();
787 if (lastModifiedTime != lastSyncTime) {
788 // Prefs at this node were externally modified; read in node and
789 // playback any local mods since last sync
790 loadCache();
791 replayChanges();
792 lastSyncTime = lastModifiedTime;
793 }
794 } else if (lastSyncTime != 0 && !dir.exists()) {
795 // This node was removed in the background. Playback any changes
796 // against a virgin (empty) Map.
797 prefsCache = new TreeMap<>();
798 replayChanges();
799 }
800 if (!changeLog.isEmpty()) {
801 writeBackCache(); // Creates directory & file if necessary
802 /*
803 * Attempt succeeded; it's barely possible that the call to
804 * lastModified might fail (i.e., return 0), but this would not
805 * be a disaster, as lastSyncTime is allowed to lag.
806 */
807 lastModifiedTime = prefsFile.lastModified();
808 /* If lastSyncTime did not change, or went back
809 * increment by 1 second. Since we hold the lock
810 * lastSyncTime always monotonically encreases in the
811 * atomic sense.
812 */
813 if (lastSyncTime <= lastModifiedTime) {
814 lastSyncTime = lastModifiedTime + 1000;
815 prefsFile.setLastModified(lastSyncTime);
816 }
817 changeLog.clear();
818 }
819 }
820
821 public void flush() throws BackingStoreException {
822 if (isRemoved())
823 return;
824 sync();
825 }
826
827 protected void flushSpi() throws BackingStoreException {
828 // assert false;
829 }
830
831 /**
832 * Returns true if the specified character is appropriate for use in
833 * Unix directory names. A character is appropriate if it's a printable
834 * ASCII character (> 0x1f && < 0x7f) and unequal to slash ('/', 0x2f),
835 * dot ('.', 0x2e), or underscore ('_', 0x5f).
836 */
837 private static boolean isDirChar(char ch) {
838 return ch > 0x1f && ch < 0x7f && ch != '/' && ch != '.' && ch != '_';
839 }
840
841 /**
842 * Returns the directory name corresponding to the specified node name.
843 * Generally, this is just the node name. If the node name includes
844 * inappropriate characters (as per isDirChar) it is translated to Base64.
845 * with the underscore character ('_', 0x5f) prepended.
846 */
847 private static String dirName(String nodeName) {
848 for (int i=0, n=nodeName.length(); i < n; i++)
849 if (!isDirChar(nodeName.charAt(i)))
850 return "_" + Base64.byteArrayToAltBase64(byteArray(nodeName));
851 return nodeName;
852 }
853
854 /**
855 * Translate a string into a byte array by translating each character
856 * into two bytes, high-byte first ("big-endian").
857 */
858 private static byte[] byteArray(String s) {
859 int len = s.length();
860 byte[] result = new byte[2*len];
861 for (int i=0, j=0; i<len; i++) {
862 char c = s.charAt(i);
863 result[j++] = (byte) (c>>8);
864 result[j++] = (byte) c;
865 }
866 return result;
867 }
868
869 /**
870 * Returns the node name corresponding to the specified directory name.
871 * (Inverts the transformation of dirName(String).
872 */
873 private static String nodeName(String dirName) {
874 if (dirName.charAt(0) != '_')
875 return dirName;
876 byte a[] = Base64.altBase64ToByteArray(dirName.substring(1));
877 StringBuffer result = new StringBuffer(a.length/2);
878 for (int i = 0; i < a.length; ) {
879 int highByte = a[i++] & 0xff;
880 int lowByte = a[i++] & 0xff;
881 result.append((char) ((highByte << 8) | lowByte));
882 }
883 return result.toString();
884 }
885
886 /**
887 * Try to acquire the appropriate file lock (user or system). If
888 * the initial attempt fails, several more attempts are made using
889 * an exponential backoff strategy. If all attempts fail, this method
890 * returns false.
891 * @throws SecurityException if file access denied.
892 */
893 private boolean lockFile(boolean shared) throws SecurityException{
894 boolean usernode = isUserNode();
895 int[] result;
896 int errorCode = 0;
897 File lockFile = (usernode ? userLockFile : systemLockFile);
898 long sleepTime = INIT_SLEEP_TIME;
899 for (int i = 0; i < MAX_ATTEMPTS; i++) {
900 try {
901 int perm = (usernode? USER_READ_WRITE: USER_RW_ALL_READ);
902 result = lockFile0(lockFile.getCanonicalPath(), perm, shared);
903
904 errorCode = result[ERROR_CODE];
905 if (result[LOCK_HANDLE] != 0) {
906 if (usernode) {
907 userRootLockHandle = result[LOCK_HANDLE];
908 } else {
909 systemRootLockHandle = result[LOCK_HANDLE];
910 }
911 return true;
912 }
913 } catch(IOException e) {
914 // // If at first, you don't succeed...
915 }
916
917 try {
918 Thread.sleep(sleepTime);
919 } catch(InterruptedException e) {
920 checkLockFile0ErrorCode(errorCode);
921 return false;
922 }
923 sleepTime *= 2;
924 }
925 checkLockFile0ErrorCode(errorCode);
926 return false;
927 }
928
929 /**
930 * Checks if unlockFile0() returned an error. Throws a SecurityException,
931 * if access denied. Logs a warning otherwise.
932 */
933 private void checkLockFile0ErrorCode (int errorCode)
934 throws SecurityException {
935 if (errorCode == EACCES)
936 throw new SecurityException("Could not lock " +
937 (isUserNode()? "User prefs." : "System prefs.") +
938 " Lock file access denied.");
939 if (errorCode != EAGAIN)
940 getLogger().warning("Could not lock " +
941 (isUserNode()? "User prefs. " : "System prefs.") +
942 " Unix error code " + errorCode + ".");
943 }
944
945 /**
946 * Locks file using UNIX file locking.
947 * @param fileName Absolute file name of the lock file.
948 * @return Returns a lock handle, used to unlock the file.
949 */
950 private static native int[]
951 lockFile0(String fileName, int permission, boolean shared);
952
953 /**
954 * Unlocks file previously locked by lockFile0().
955 * @param lockHandle Handle to the file lock.
956 * @return Returns zero if OK, UNIX error code if failure.
957 */
958 private static native int unlockFile0(int lockHandle);
959
960 /**
961 * Changes UNIX file permissions.
962 */
963 private static native int chmod(String fileName, int permission);
964
965 /**
966 * Initial time between lock attempts, in ms. The time is doubled
967 * after each failing attempt (except the first).
968 */
969 private static int INIT_SLEEP_TIME = 50;
970
971 /**
972 * Maximum number of lock attempts.
973 */
974 private static int MAX_ATTEMPTS = 5;
975
976 /**
977 * Release the the appropriate file lock (user or system).
978 * @throws SecurityException if file access denied.
979 */
980 private void unlockFile() {
981 int result;
982 boolean usernode = isUserNode();
983 File lockFile = (usernode ? userLockFile : systemLockFile);
984 int lockHandle = ( usernode ? userRootLockHandle:systemRootLockHandle);
985 if (lockHandle == 0) {
986 getLogger().warning("Unlock: zero lockHandle for " +
987 (usernode ? "user":"system") + " preferences.)");
988 return;
989 }
990 result = unlockFile0(lockHandle);
991 if (result != 0) {
992 getLogger().warning("Could not drop file-lock on " +
993 (isUserNode() ? "user" : "system") + " preferences." +
994 " Unix error code " + result + ".");
995 if (result == EACCES)
996 throw new SecurityException("Could not unlock" +
997 (isUserNode()? "User prefs." : "System prefs.") +
998 " Lock file access denied.");
999 }
1000 if (isUserNode()) {
1001 userRootLockHandle = 0;
1002 } else {
1003 systemRootLockHandle = 0;
1004 }
1005 }
1006 }