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