blob: 4b5b6d20d427e594b8b31bb643c155b95f0feb43 [file] [log] [blame]
J. Duke319a3b92007-12-01 00:00:00 +00001/*
2 * Copyright 2000-2006 Sun Microsystems, Inc. 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. Sun designates this
8 * particular file as subject to the "Classpath" exception as provided
9 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
22 * CA 95054 USA or visit www.sun.com if you need additional information or
23 * have any questions.
24 */
25
26package java.util.prefs;
27import java.util.*;
28import java.io.*;
29import java.util.logging.Logger;
30import java.security.AccessController;
31import java.security.PrivilegedAction;
32import java.security.PrivilegedExceptionAction;
33import java.security.PrivilegedActionException;
34
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 */
50class FileSystemPreferences extends AbstractPreferences {
51 /**
52 * Sync interval in seconds.
53 */
54 private static final int SYNC_INTERVAL = Math.max(1,
55 Integer.parseInt((String)
56 AccessController.doPrivileged(new PrivilegedAction() {
57 public Object run() {
58 return System.getProperty("java.util.prefs.syncInterval",
59 "30");
60 }
61 })));
62
63
64 /**
65 * Returns logger for error messages. Backing store exceptions are logged at
66 * WARNING level.
67 */
68 private static Logger getLogger() {
69 return Logger.getLogger("java.util.prefs");
70 }
71
72 /**
73 * Directory for system preferences.
74 */
75 private static File systemRootDir;
76
77 /*
78 * Flag, indicating whether systemRoot directory is writable
79 */
80 private static boolean isSystemRootWritable;
81
82 /**
83 * Directory for user preferences.
84 */
85 private static File userRootDir;
86
87 /*
88 * Flag, indicating whether userRoot directory is writable
89 */
90 private static boolean isUserRootWritable;
91
92 /**
93 * The user root.
94 */
95 static Preferences userRoot = null;
96
97 static synchronized Preferences getUserRoot() {
98 if (userRoot == null) {
99 setupUserRoot();
100 userRoot = new FileSystemPreferences(true);
101 }
102 return userRoot;
103 }
104
105 private static void setupUserRoot() {
106 AccessController.doPrivileged(new PrivilegedAction() {
107 public Object run() {
108 userRootDir =
109 new File(System.getProperty("java.util.prefs.userRoot",
110 System.getProperty("user.home")), ".java/.userPrefs");
111 // Attempt to create root dir if it does not yet exist.
112 if (!userRootDir.exists()) {
113 if (userRootDir.mkdirs()) {
114 try {
115 chmod(userRootDir.getCanonicalPath(), USER_RWX);
116 } catch (IOException e) {
117 getLogger().warning("Could not change permissions" +
118 " on userRoot directory. ");
119 }
120 getLogger().info("Created user preferences directory.");
121 }
122 else
123 getLogger().warning("Couldn't create user preferences" +
124 " directory. User preferences are unusable.");
125 }
126 isUserRootWritable = userRootDir.canWrite();
127 String USER_NAME = System.getProperty("user.name");
128 userLockFile = new File (userRootDir,".user.lock." + USER_NAME);
129 userRootModFile = new File (userRootDir,
130 ".userRootModFile." + USER_NAME);
131 if (!userRootModFile.exists())
132 try {
133 // create if does not exist.
134 userRootModFile.createNewFile();
135 // Only user can read/write userRootModFile.
136 int result = chmod(userRootModFile.getCanonicalPath(),
137 USER_READ_WRITE);
138 if (result !=0)
139 getLogger().warning("Problem creating userRoot " +
140 "mod file. Chmod failed on " +
141 userRootModFile.getCanonicalPath() +
142 " Unix error code " + result);
143 } catch (IOException e) {
144 getLogger().warning(e.toString());
145 }
146 userRootModTime = userRootModFile.lastModified();
147 return null;
148 }
149 });
150 }
151
152
153 /**
154 * The system root.
155 */
156 static Preferences systemRoot;
157
158 static synchronized Preferences getSystemRoot() {
159 if (systemRoot == null) {
160 setupSystemRoot();
161 systemRoot = new FileSystemPreferences(false);
162 }
163 return systemRoot;
164 }
165
166 private static void setupSystemRoot() {
167 AccessController.doPrivileged( new PrivilegedAction() {
168 public Object run() {
169 String systemPrefsDirName = (String)
170 System.getProperty("java.util.prefs.systemRoot","/etc/.java");
171 systemRootDir =
172 new File(systemPrefsDirName, ".systemPrefs");
173 // Attempt to create root dir if it does not yet exist.
174 if (!systemRootDir.exists()) {
175 // system root does not exist in /etc/.java
176 // Switching to java.home
177 systemRootDir =
178 new File(System.getProperty("java.home"),
179 ".systemPrefs");
180 if (!systemRootDir.exists()) {
181 if (systemRootDir.mkdirs()) {
182 getLogger().info(
183 "Created system preferences directory "
184 + "in java.home.");
185 try {
186 chmod(systemRootDir.getCanonicalPath(),
187 USER_RWX_ALL_RX);
188 } catch (IOException e) {
189 }
190 } else {
191 getLogger().warning("Could not create "
192 + "system preferences directory. System "
193 + "preferences are unusable.");
194 }
195 }
196 }
197 isSystemRootWritable = systemRootDir.canWrite();
198 systemLockFile = new File(systemRootDir, ".system.lock");
199 systemRootModFile =
200 new File (systemRootDir,".systemRootModFile");
201 if (!systemRootModFile.exists() && isSystemRootWritable)
202 try {
203 // create if does not exist.
204 systemRootModFile.createNewFile();
205 int result = chmod(systemRootModFile.getCanonicalPath(),
206 USER_RW_ALL_READ);
207 if (result !=0)
208 getLogger().warning("Chmod failed on " +
209 systemRootModFile.getCanonicalPath() +
210 " Unix error code " + result);
211 } catch (IOException e) { getLogger().warning(e.toString());
212 }
213 systemRootModTime = systemRootModFile.lastModified();
214 return null;
215 }
216 });
217 }
218
219
220 /**
221 * Unix user write/read permission
222 */
223 private static final int USER_READ_WRITE = 0600;
224
225 private static final int USER_RW_ALL_READ = 0644;
226
227
228 private static final int USER_RWX_ALL_RX = 0755;
229
230 private static final int USER_RWX = 0700;
231
232 /**
233 * The lock file for the user tree.
234 */
235 static File userLockFile;
236
237
238
239 /**
240 * The lock file for the system tree.
241 */
242 static File systemLockFile;
243
244 /**
245 * Unix lock handle for userRoot.
246 * Zero, if unlocked.
247 */
248
249 private static int userRootLockHandle = 0;
250
251 /**
252 * Unix lock handle for systemRoot.
253 * Zero, if unlocked.
254 */
255
256 private static int systemRootLockHandle = 0;
257
258 /**
259 * The directory representing this preference node. There is no guarantee
260 * that this directory exits, as another VM can delete it at any time
261 * that it (the other VM) holds the file-lock. While the root node cannot
262 * be deleted, it may not yet have been created, or the underlying
263 * directory could have been deleted accidentally.
264 */
265 private final File dir;
266
267 /**
268 * The file representing this preference node's preferences.
269 * The file format is undocumented, and subject to change
270 * from release to release, but I'm sure that you can figure
271 * it out if you try real hard.
272 */
273 private final File prefsFile;
274
275 /**
276 * A temporary file used for saving changes to preferences. As part of
277 * the sync operation, changes are first saved into this file, and then
278 * atomically renamed to prefsFile. This results in an atomic state
279 * change from one valid set of preferences to another. The
280 * the file-lock is held for the duration of this transformation.
281 */
282 private final File tmpFile;
283
284 /**
285 * File, which keeps track of global modifications of userRoot.
286 */
287 private static File userRootModFile;
288
289 /**
290 * Flag, which indicated whether userRoot was modified by another VM
291 */
292 private static boolean isUserRootModified = false;
293
294 /**
295 * Keeps track of userRoot modification time. This time is reset to
296 * zero after UNIX reboot, and is increased by 1 second each time
297 * userRoot is modified.
298 */
299 private static long userRootModTime;
300
301
302 /*
303 * File, which keeps track of global modifications of systemRoot
304 */
305 private static File systemRootModFile;
306 /*
307 * Flag, which indicates whether systemRoot was modified by another VM
308 */
309 private static boolean isSystemRootModified = false;
310
311 /**
312 * Keeps track of systemRoot modification time. This time is reset to
313 * zero after system reboot, and is increased by 1 second each time
314 * systemRoot is modified.
315 */
316 private static long systemRootModTime;
317
318 /**
319 * Locally cached preferences for this node (includes uncommitted
320 * changes). This map is initialized with from disk when the first get or
321 * put operation occurs on this node. It is synchronized with the
322 * corresponding disk file (prefsFile) by the sync operation. The initial
323 * value is read *without* acquiring the file-lock.
324 */
325 private Map prefsCache = null;
326
327 /**
328 * The last modification time of the file backing this node at the time
329 * that prefCache was last synchronized (or initially read). This
330 * value is set *before* reading the file, so it's conservative; the
331 * actual timestamp could be (slightly) higher. A value of zero indicates
332 * that we were unable to initialize prefsCache from the disk, or
333 * have not yet attempted to do so. (If prefsCache is non-null, it
334 * indicates the former; if it's null, the latter.)
335 */
336 private long lastSyncTime = 0;
337
338 /**
339 * Unix error code for locked file.
340 */
341 private static final int EAGAIN = 11;
342
343 /**
344 * Unix error code for denied access.
345 */
346 private static final int EACCES = 13;
347
348 /* Used to interpret results of native functions */
349 private static final int LOCK_HANDLE = 0;
350 private static final int ERROR_CODE = 1;
351
352 /**
353 * A list of all uncommitted preference changes. The elements in this
354 * list are of type PrefChange. If this node is concurrently modified on
355 * disk by another VM, the two sets of changes are merged when this node
356 * is sync'ed by overwriting our prefsCache with the preference map last
357 * written out to disk (by the other VM), and then replaying this change
358 * log against that map. The resulting map is then written back
359 * to the disk.
360 */
361 final List changeLog = new ArrayList();
362
363 /**
364 * Represents a change to a preference.
365 */
366 private abstract class Change {
367 /**
368 * Reapplies the change to prefsCache.
369 */
370 abstract void replay();
371 };
372
373 /**
374 * Represents a preference put.
375 */
376 private class Put extends Change {
377 String key, value;
378
379 Put(String key, String value) {
380 this.key = key;
381 this.value = value;
382 }
383
384 void replay() {
385 prefsCache.put(key, value);
386 }
387 }
388
389 /**
390 * Represents a preference remove.
391 */
392 private class Remove extends Change {
393 String key;
394
395 Remove(String key) {
396 this.key = key;
397 }
398
399 void replay() {
400 prefsCache.remove(key);
401 }
402 }
403
404 /**
405 * Represents the creation of this node.
406 */
407 private class NodeCreate extends Change {
408 /**
409 * Performs no action, but the presence of this object in changeLog
410 * will force the node and its ancestors to be made permanent at the
411 * next sync.
412 */
413 void replay() {
414 }
415 }
416
417 /**
418 * NodeCreate object for this node.
419 */
420 NodeCreate nodeCreate = null;
421
422 /**
423 * Replay changeLog against prefsCache.
424 */
425 private void replayChanges() {
426 for (int i = 0, n = changeLog.size(); i<n; i++)
427 ((Change)changeLog.get(i)).replay();
428 }
429
430 private static Timer syncTimer = new Timer(true); // Daemon Thread
431
432 static {
433 // Add periodic timer task to periodically sync cached prefs
434 syncTimer.schedule(new TimerTask() {
435 public void run() {
436 syncWorld();
437 }
438 }, SYNC_INTERVAL*1000, SYNC_INTERVAL*1000);
439
440 // Add shutdown hook to flush cached prefs on normal termination
441 AccessController.doPrivileged(new PrivilegedAction() {
442 public Object run() {
443 Runtime.getRuntime().addShutdownHook(new Thread() {
444 public void run() {
445 syncTimer.cancel();
446 syncWorld();
447 }
448 });
449 return null;
450 }
451 });
452 }
453
454 private static void syncWorld() {
455 /*
456 * Synchronization necessary because userRoot and systemRoot are
457 * lazily initialized.
458 */
459 Preferences userRt;
460 Preferences systemRt;
461 synchronized(FileSystemPreferences.class) {
462 userRt = userRoot;
463 systemRt = systemRoot;
464 }
465
466 try {
467 if (userRt != null)
468 userRt.flush();
469 } catch(BackingStoreException e) {
470 getLogger().warning("Couldn't flush user prefs: " + e);
471 }
472
473 try {
474 if (systemRt != null)
475 systemRt.flush();
476 } catch(BackingStoreException e) {
477 getLogger().warning("Couldn't flush system prefs: " + e);
478 }
479 }
480
481 private final boolean isUserNode;
482
483 /**
484 * Special constructor for roots (both user and system). This constructor
485 * will only be called twice, by the static initializer.
486 */
487 private FileSystemPreferences(boolean user) {
488 super(null, "");
489 isUserNode = user;
490 dir = (user ? userRootDir: systemRootDir);
491 prefsFile = new File(dir, "prefs.xml");
492 tmpFile = new File(dir, "prefs.tmp");
493 }
494
495 /**
496 * Construct a new FileSystemPreferences instance with the specified
497 * parent node and name. This constructor, called from childSpi,
498 * is used to make every node except for the two //roots.
499 */
500 private FileSystemPreferences(FileSystemPreferences parent, String name) {
501 super(parent, name);
502 isUserNode = parent.isUserNode;
503 dir = new File(parent.dir, dirName(name));
504 prefsFile = new File(dir, "prefs.xml");
505 tmpFile = new File(dir, "prefs.tmp");
506 AccessController.doPrivileged( new PrivilegedAction() {
507 public Object run() {
508 newNode = !dir.exists();
509 return null;
510 }
511 });
512 if (newNode) {
513 // These 2 things guarantee node will get wrtten at next flush/sync
514 prefsCache = new TreeMap();
515 nodeCreate = new NodeCreate();
516 changeLog.add(nodeCreate);
517 }
518 }
519
520 public boolean isUserNode() {
521 return isUserNode;
522 }
523
524 protected void putSpi(String key, String value) {
525 initCacheIfNecessary();
526 changeLog.add(new Put(key, value));
527 prefsCache.put(key, value);
528 }
529
530 protected String getSpi(String key) {
531 initCacheIfNecessary();
532 return (String) prefsCache.get(key);
533 }
534
535 protected void removeSpi(String key) {
536 initCacheIfNecessary();
537 changeLog.add(new Remove(key));
538 prefsCache.remove(key);
539 }
540
541 /**
542 * Initialize prefsCache if it has yet to be initialized. When this method
543 * returns, prefsCache will be non-null. If the data was successfully
544 * read from the file, lastSyncTime will be updated. If prefsCache was
545 * null, but it was impossible to read the file (because it didn't
546 * exist or for any other reason) prefsCache will be initialized to an
547 * empty, modifiable Map, and lastSyncTime remain zero.
548 */
549 private void initCacheIfNecessary() {
550 if (prefsCache != null)
551 return;
552
553 try {
554 loadCache();
555 } catch(Exception e) {
556 // assert lastSyncTime == 0;
557 prefsCache = new TreeMap();
558 }
559 }
560
561 /**
562 * Attempt to load prefsCache from the backing store. If the attempt
563 * succeeds, lastSyncTime will be updated (the new value will typically
564 * correspond to the data loaded into the map, but it may be less,
565 * if another VM is updating this node concurrently). If the attempt
566 * fails, a BackingStoreException is thrown and both prefsCache and
567 * lastSyncTime are unaffected by the call.
568 */
569 private void loadCache() throws BackingStoreException {
570 try {
571 AccessController.doPrivileged( new PrivilegedExceptionAction() {
572 public Object run() throws BackingStoreException {
573 Map m = new TreeMap();
574 long newLastSyncTime = 0;
575 try {
576 newLastSyncTime = prefsFile.lastModified();
577 FileInputStream fis = new FileInputStream(prefsFile);
578 XmlSupport.importMap(fis, m);
579 fis.close();
580 } catch(Exception e) {
581 if (e instanceof InvalidPreferencesFormatException) {
582 getLogger().warning("Invalid preferences format in "
583 + prefsFile.getPath());
584 prefsFile.renameTo( new File(
585 prefsFile.getParentFile(),
586 "IncorrectFormatPrefs.xml"));
587 m = new TreeMap();
588 } else if (e instanceof FileNotFoundException) {
589 getLogger().warning("Prefs file removed in background "
590 + prefsFile.getPath());
591 } else {
592 throw new BackingStoreException(e);
593 }
594 }
595 // Attempt succeeded; update state
596 prefsCache = m;
597 lastSyncTime = newLastSyncTime;
598 return null;
599 }
600 });
601 } catch (PrivilegedActionException e) {
602 throw (BackingStoreException) e.getException();
603 }
604 }
605
606 /**
607 * Attempt to write back prefsCache to the backing store. If the attempt
608 * succeeds, lastSyncTime will be updated (the new value will correspond
609 * exactly to the data thust written back, as we hold the file lock, which
610 * prevents a concurrent write. If the attempt fails, a
611 * BackingStoreException is thrown and both the backing store (prefsFile)
612 * and lastSyncTime will be unaffected by this call. This call will
613 * NEVER leave prefsFile in a corrupt state.
614 */
615 private void writeBackCache() throws BackingStoreException {
616 try {
617 AccessController.doPrivileged( new PrivilegedExceptionAction() {
618 public Object run() throws BackingStoreException {
619 try {
620 if (!dir.exists() && !dir.mkdirs())
621 throw new BackingStoreException(dir +
622 " create failed.");
623 FileOutputStream fos = new FileOutputStream(tmpFile);
624 XmlSupport.exportMap(fos, prefsCache);
625 fos.close();
626 if (!tmpFile.renameTo(prefsFile))
627 throw new BackingStoreException("Can't rename " +
628 tmpFile + " to " + prefsFile);
629 } catch(Exception e) {
630 if (e instanceof BackingStoreException)
631 throw (BackingStoreException)e;
632 throw new BackingStoreException(e);
633 }
634 return null;
635 }
636 });
637 } catch (PrivilegedActionException e) {
638 throw (BackingStoreException) e.getException();
639 }
640 }
641
642 protected String[] keysSpi() {
643 initCacheIfNecessary();
644 return (String[])
645 prefsCache.keySet().toArray(new String[prefsCache.size()]);
646 }
647
648 protected String[] childrenNamesSpi() {
649 return (String[])
650 AccessController.doPrivileged( new PrivilegedAction() {
651 public Object run() {
652 List result = new ArrayList();
653 File[] dirContents = dir.listFiles();
654 if (dirContents != null) {
655 for (int i = 0; i < dirContents.length; i++)
656 if (dirContents[i].isDirectory())
657 result.add(nodeName(dirContents[i].getName()));
658 }
659 return result.toArray(EMPTY_STRING_ARRAY);
660 }
661 });
662 }
663
664 private static final String[] EMPTY_STRING_ARRAY = new String[0];
665
666 protected AbstractPreferences childSpi(String name) {
667 return new FileSystemPreferences(this, name);
668 }
669
670 public void removeNode() throws BackingStoreException {
671 synchronized (isUserNode()? userLockFile: systemLockFile) {
672 // to remove a node we need an exclusive lock
673 if (!lockFile(false))
674 throw(new BackingStoreException("Couldn't get file lock."));
675 try {
676 super.removeNode();
677 } finally {
678 unlockFile();
679 }
680 }
681 }
682
683 /**
684 * Called with file lock held (in addition to node locks).
685 */
686 protected void removeNodeSpi() throws BackingStoreException {
687 try {
688 AccessController.doPrivileged( new PrivilegedExceptionAction() {
689 public Object run() throws BackingStoreException {
690 if (changeLog.contains(nodeCreate)) {
691 changeLog.remove(nodeCreate);
692 nodeCreate = null;
693 return null;
694 }
695 if (!dir.exists())
696 return null;
697 prefsFile.delete();
698 tmpFile.delete();
699 // dir should be empty now. If it's not, empty it
700 File[] junk = dir.listFiles();
701 if (junk.length != 0) {
702 getLogger().warning(
703 "Found extraneous files when removing node: "
704 + Arrays.asList(junk));
705 for (int i=0; i<junk.length; i++)
706 junk[i].delete();
707 }
708 if (!dir.delete())
709 throw new BackingStoreException("Couldn't delete dir: "
710 + dir);
711 return null;
712 }
713 });
714 } catch (PrivilegedActionException e) {
715 throw (BackingStoreException) e.getException();
716 }
717 }
718
719 public synchronized void sync() throws BackingStoreException {
720 boolean userNode = isUserNode();
721 boolean shared;
722
723 if (userNode) {
724 shared = false; /* use exclusive lock for user prefs */
725 } else {
726 /* if can write to system root, use exclusive lock.
727 otherwise use shared lock. */
728 shared = !isSystemRootWritable;
729 }
730 synchronized (isUserNode()? userLockFile:systemLockFile) {
731 if (!lockFile(shared))
732 throw(new BackingStoreException("Couldn't get file lock."));
733 final Long newModTime =
734 (Long) AccessController.doPrivileged( new PrivilegedAction() {
735 public Object run() {
736 long nmt;
737 if (isUserNode()) {
738 nmt = userRootModFile.lastModified();
739 isUserRootModified = userRootModTime == nmt;
740 } else {
741 nmt = systemRootModFile.lastModified();
742 isSystemRootModified = systemRootModTime == nmt;
743 }
744 return new Long(nmt);
745 }
746 });
747 try {
748 super.sync();
749 AccessController.doPrivileged( new PrivilegedAction() {
750 public Object run() {
751 if (isUserNode()) {
752 userRootModTime = newModTime.longValue() + 1000;
753 userRootModFile.setLastModified(userRootModTime);
754 } else {
755 systemRootModTime = newModTime.longValue() + 1000;
756 systemRootModFile.setLastModified(systemRootModTime);
757 }
758 return null;
759 }
760 });
761 } finally {
762 unlockFile();
763 }
764 }
765 }
766
767 protected void syncSpi() throws BackingStoreException {
768 try {
769 AccessController.doPrivileged( new PrivilegedExceptionAction() {
770 public Object 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}