001: /*
002: * @(#)FileSystemPreferences.java 1.21 05/11/17
003: *
004: * Copyright 2006 Sun Microsystems, Inc. All rights reserved.
005: * SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
006: */
007:
008: package org.wings.prefs;
009:
010: import org.w3c.dom.Element;
011: import org.w3c.dom.NodeList;
012:
013: import javax.servlet.http.*;
014: import javax.xml.parsers.DocumentBuilder;
015: import javax.xml.parsers.DocumentBuilderFactory;
016: import javax.xml.transform.Transformer;
017: import javax.xml.transform.TransformerFactory;
018: import javax.xml.transform.dom.DOMSource;
019: import javax.xml.transform.stream.StreamResult;
020: import java.io.*;
021: import java.util.*;
022: import java.util.logging.Logger;
023: import java.util.prefs.*;
024:
025: /**
026: * ClientPreferences implementation for Servlets. ClientPreferences are stored in the file
027: * system, with one directory per preferences node. All of the preferences
028: * at each node are stored in a single file. Atomic file system operations
029: * (e.g. File.renameTo) are used to ensure integrity. An in-memory cache of
030: * the "explored" portion of the tree is maintained for performance, and
031: * written back to the disk periodically. File-locking is used to ensure
032: * reasonable behavior when multiple VMs are running at the same time.
033: * (The file lock is obtained only for sync(), flush() and removeNode().)
034: *
035: * @author Christian
036: * @version 1.21, 11/17/05
037: * @see Preferences
038: * @since 1.4
039: */
040: public class ServletPreferences extends
041: java.util.prefs.AbstractPreferences {
042:
043: /**
044: * Name for the cookies containing the users' state
045: */
046: private static String COOKIE_NAME = "PreferencesCookie";
047:
048: /**
049: * Key for the next unused User ID in the System ClientPreferences
050: */
051: private static String NEXT_FREE_USER_ID = "nextFreeUserID";
052: static final ThreadLocal<HttpServletRequest> requests = new ThreadLocal<HttpServletRequest>();
053: static final ThreadLocal<HttpServletResponse> responses = new ThreadLocal<HttpServletResponse>();
054:
055: /**
056: * Returns logger for error messages. Backing store exceptions are logged at
057: * WARNING level.
058: */
059: private static Logger getLogger() {
060: return Logger.getLogger("java.util.prefs");
061: }
062:
063: /**
064: * Directory for system preferences.
065: */
066: private static File systemRootDir;
067:
068: /*
069: * Flag, indicating whether systemRoot directory is writable
070: */
071: private static boolean isSystemRootWritable;
072:
073: /**
074: * the user roots
075: */
076: private static Map<String, ServletPreferences> userRoots = new HashMap<String, ServletPreferences>();
077:
078: static synchronized Preferences getUserRoot() {
079: String userName = resolveUserName();
080:
081: if (userRoots.containsKey(userName))
082: return userRoots.get(userName);
083:
084: ServletPreferences userRoot = new ServletPreferences(true);
085: userRoots.put(userName, userRoot);
086: return userRoot;
087:
088: }
089:
090: private static synchronized String resolveUserName() {
091: String userName = "user/";
092:
093: HttpServletRequest request = requests.get();
094: //if authorized user, use this name
095: if (request.getUserPrincipal() != null
096: && request.getUserPrincipal().getName() != null) {
097: userName = request.getUserPrincipal().getName();
098:
099: } else {
100: boolean isAlreadyKnown = false;
101: userName = (String) request.getSession().getAttribute(
102: COOKIE_NAME);
103: if (userName != null)
104: isAlreadyKnown = true;
105: else {
106: Cookie[] cookies = request.getCookies();
107: if (cookies != null) {
108: for (int i = 0; i < cookies.length; i++) {
109: if (cookies[i].getName().equals(COOKIE_NAME)) {
110: //pref = ClientPreferences.userRoot().node(cookies[i].getValue());
111: userName = cookies[i].getValue();
112: request.getSession().setAttribute(
113: COOKIE_NAME, userName);
114: isAlreadyKnown = true;
115: break;
116: }
117: }
118: }
119: }
120:
121: if (!isAlreadyKnown) {
122: //pref = ClientPreferences.userRoot().node(userID.toString());
123: int userID = getSystemRoot().getInt(NEXT_FREE_USER_ID,
124: 0);
125: userName = ((Integer) userID).toString();
126:
127: //Set the cookie
128: Cookie cookie = new Cookie(COOKIE_NAME, userName);
129: cookie.setMaxAge(1000000000);
130: responses.get().addCookie(cookie);
131: request.getSession()
132: .setAttribute(COOKIE_NAME, userName);
133:
134: userID++;
135: getSystemRoot().putInt(NEXT_FREE_USER_ID, userID);
136:
137: try {
138: getSystemRoot().flush();
139:
140: } catch (Exception ex) {
141: ex.printStackTrace();
142: }
143:
144: }
145:
146: }
147:
148: return userName;
149:
150: }
151:
152: private static File setupUserRoot(String userName) {
153:
154: File userRootDir = new File("./Prefs/" + userName);
155: // new File(System.getProperty("java.util.prefs.userRoot",
156: // System.getProperty("user.home")), ".java/.userPrefs");
157: // Attempt to create root dir if it does not yet exist.
158: if (!userRootDir.exists()) {
159: if (userRootDir.mkdirs()) {
160:
161: getLogger().info("Created user preferences directory.");
162: } else
163: getLogger()
164: .warning(
165: "Couldn't create user preferences"
166: + " directory. User preferences are unusable.");
167: }
168:
169: String USER_NAME = System.getProperty("user.name");
170: userLockFile = new File(userRootDir, ".user.lock." + USER_NAME);
171: userRootModFile = new File(userRootDir, ".userRootModFile."
172: + USER_NAME);
173: if (!userRootModFile.exists())
174: try {
175: // create if does not exist.
176: userRootModFile.createNewFile();
177: } catch (IOException e) {
178: getLogger().warning(e.toString());
179: }
180: userRootModTime = userRootModFile.lastModified();
181: return userRootDir;
182: }
183:
184: /**
185: * The system root.
186: */
187: static Preferences systemRoot;
188:
189: static synchronized Preferences getSystemRoot() {
190: if (systemRoot == null) {
191: setupSystemRoot();
192: systemRoot = new ServletPreferences(false);
193: }
194: return systemRoot;
195: }
196:
197: private static void setupSystemRoot() {
198: String systemPrefsDirName = "./Prefs/system/";
199:
200: //(String)System.getProperty("java.util.prefs.systemRoot","/etc/.java");
201:
202: systemRootDir = new File(systemPrefsDirName);//, ".systemPrefs");
203: // Attempt to create root dir if it does not yet exist.
204: //if (!systemRootDir.exists()) {
205: // system root does not exist in /etc/.java
206: // Switching to java.home
207: //systemRootDir = new File(System.getProperty("java.home"),".systemPrefs");
208: if (!systemRootDir.exists()) {
209: if (systemRootDir.mkdirs()) {
210: getLogger().info(
211: "Created system preferences directory " + "in "
212: + systemPrefsDirName);
213:
214: } else {
215: getLogger()
216: .warning(
217: "Could not create "
218: + "system preferences directory. System "
219: + "preferences are unusable.");
220: }
221:
222: }
223: isSystemRootWritable = systemRootDir.canWrite();
224: systemLockFile = new File(systemRootDir, ".system.lock");
225: systemRootModFile = new File(systemRootDir,
226: ".systemRootModFile");
227: if (!systemRootModFile.exists() && isSystemRootWritable)
228: try {
229: // create if does not exist.
230: systemRootModFile.createNewFile();
231:
232: } catch (IOException e) {
233: getLogger().warning(e.toString());
234: }
235: systemRootModTime = systemRootModFile.lastModified();
236:
237: }
238:
239: /**
240: * The lock file for the user tree.
241: */
242: static File userLockFile;
243:
244: /**
245: * The lock file for the system tree.
246: */
247: static File systemLockFile;
248:
249: /**
250: * The directory representing this preference node. There is no guarantee
251: * that this directory exits, as another VM can delete it at any time
252: * that it (the other VM) holds the file-lock. While the root node cannot
253: * be deleted, it may not yet have been created, or the underlying
254: * directory could have been deleted accidentally.
255: */
256: private final File dir;
257:
258: /**
259: * The file representing this preference node's preferences.
260: * The file format is undocumented, and subject to change
261: * from release to release, but I'm sure that you can figure
262: * it out if you try real hard.
263: */
264: private final File prefsFile;
265:
266: /**
267: * A temporary file used for saving changes to preferences. As part of
268: * the sync operation, changes are first saved into this file, and then
269: * atomically renamed to prefsFile. This results in an atomic state
270: * change from one valid set of preferences to another. The
271: * the file-lock is held for the duration of this transformation.
272: */
273: private final File tmpFile;
274:
275: /**
276: * File, which keeps track of global modifications of userRoot.
277: */
278: private static File userRootModFile;
279:
280: /**
281: * Flag, which indicated whether userRoot was modified by another VM
282: */
283: private static boolean isUserRootModified = false;
284:
285: /**
286: * Keeps track of userRoot modification time. This time is reset to
287: * zero after UNIX reboot, and is increased by 1 second each time
288: * userRoot is modified.
289: */
290: private static long userRootModTime;
291:
292: /*
293: * File, which keeps track of global modifications of systemRoot
294: */
295: private static File systemRootModFile;
296: /*
297: * Flag, which indicates whether systemRoot was modified by another VM
298: */
299: private static boolean isSystemRootModified = false;
300:
301: /**
302: * Keeps track of systemRoot modification time. This time is reset to
303: * zero after system reboot, and is increased by 1 second each time
304: * systemRoot is modified.
305: */
306: private static long systemRootModTime;
307:
308: /**
309: * Locally cached preferences for this node (includes uncommitted
310: * changes). This map is initialized with from disk when the first get or
311: * put operation occurs on this node. It is synchronized with the
312: * corresponding disk file (prefsFile) by the sync operation. The initial
313: * value is read *without* acquiring the file-lock.
314: */
315: private Map<String, String> prefsCache = null;
316:
317: /**
318: * The last modification time of the file backing this node at the time
319: * that prefCache was last synchronized (or initially read). This
320: * value is set *before* reading the file, so it's conservative; the
321: * actual timestamp could be (slightly) higher. A value of zero indicates
322: * that we were unable to initialize prefsCache from the disk, or
323: * have not yet attempted to do so. (If prefsCache is non-null, it
324: * indicates the former; if it's null, the latter.)
325: */
326: private long lastSyncTime = 0;
327:
328: /**
329: * A list of all uncommitted preference changes. The elements in this
330: * list are of type PrefChange. If this node is concurrently modified on
331: * disk by another VM, the two sets of changes are merged when this node
332: * is sync'ed by overwriting our prefsCache with the preference map last
333: * written out to disk (by the other VM), and then replaying this change
334: * log against that map. The resulting map is then written back
335: * to the disk.
336: */
337: final List<Change> changeLog = new ArrayList<Change>();
338:
339: public static void set(HttpServletRequest servletRequest,
340: HttpServletResponse servletResponse) {
341: requests.set(servletRequest);
342: responses.set(servletResponse);
343: }
344:
345: public static void unset() {
346: requests.set(null);
347: responses.set(null);
348: }
349:
350: /**
351: * Represents a change to a preference.
352: */
353: private abstract class Change {
354: /**
355: * Reapplies the change to prefsCache.
356: */
357: abstract void replay();
358: }
359:
360: ;
361:
362: /**
363: * Represents a preference put.
364: */
365: private class Put extends Change {
366: String key, value;
367:
368: Put(String key, String value) {
369: this .key = key;
370: this .value = value;
371: }
372:
373: void replay() {
374: prefsCache.put(key, value);
375: }
376: }
377:
378: /**
379: * Represents a preference remove.
380: */
381: private class Remove extends Change {
382: String key;
383:
384: Remove(String key) {
385: this .key = key;
386: }
387:
388: void replay() {
389: prefsCache.remove(key);
390: }
391: }
392:
393: /**
394: * Represents the creation of this node.
395: */
396: private class NodeCreate extends Change {
397: /**
398: * Performs no action, but the presence of this object in changeLog
399: * will force the node and its ancestors to be made permanent at the
400: * next sync.
401: */
402: void replay() {
403: }
404: }
405:
406: /**
407: * NodeCreate object for this node.
408: */
409: NodeCreate nodeCreate = null;
410:
411: /**
412: * Replay changeLog against prefsCache.
413: */
414: private void replayChanges() {
415: for (int i = 0, n = changeLog.size(); i < n; i++)
416: ((Change) changeLog.get(i)).replay();
417: }
418:
419: private final boolean isUserNode;
420:
421: /**
422: * Special constructor for roots (both user and system). This constructor
423: * will only be called twice, by the static initializer.
424: */
425: private ServletPreferences(boolean user) {
426: super (null, "");
427: isUserNode = user;
428:
429: dir = (user ? setupUserRoot(resolveUserName()) : systemRootDir);
430:
431: prefsFile = new File(dir, "prefs.xml");
432:
433: tmpFile = new File(dir, "prefs.tmp");
434:
435: if (newNode) {
436: // These 2 things guarantee node will get wrtten at next flush/sync
437: prefsCache = new TreeMap<String, String>();
438: nodeCreate = new NodeCreate();
439: changeLog.add(nodeCreate);
440: }
441: }
442:
443: /**
444: * Construct a new FileSystemPreferences instance with the specified
445: * parent node and name. This constructor, called from childSpi,
446: * is used to make every node except for the two //roots.
447: */
448: private ServletPreferences(ServletPreferences parent, String name) {
449: super (parent, name);
450: isUserNode = parent.isUserNode;
451: dir = new File(parent.dir, name);
452: prefsFile = new File(dir, "prefs.xml");
453: tmpFile = new File(dir, "prefs.tmp");
454: newNode = !dir.exists();
455: //this.initCacheIfNecessary();
456:
457: if (newNode) {
458: // These 2 things guarantee node will get wrtten at next flush/sync
459: prefsCache = new TreeMap<String, String>();
460: nodeCreate = new NodeCreate();
461: changeLog.add(nodeCreate);
462: }
463:
464: }
465:
466: public boolean isUserNode() {
467: return isUserNode;
468: }
469:
470: protected void putSpi(String key, String value) {
471: initCacheIfNecessary();
472: changeLog.add(new Put(key, value));
473: prefsCache.put(key, value);
474: }
475:
476: protected String getSpi(String key) {
477: initCacheIfNecessary();
478: return (String) prefsCache.get(key);
479: }
480:
481: protected void removeSpi(String key) {
482: initCacheIfNecessary();
483: changeLog.add(new Remove(key));
484: prefsCache.remove(key);
485: }
486:
487: /**
488: * Initialize prefsCache if it has yet to be initialized. When this method
489: * returns, prefsCache will be non-null. If the data was successfully
490: * read from the file, lastSyncTime will be updated. If prefsCache was
491: * null, but it was impossible to read the file (because it didn't
492: * exist or for any other reason) prefsCache will be initialized to an
493: * empty, modifiable Map, and lastSyncTime remain zero.
494: */
495: private void initCacheIfNecessary() {
496: if (prefsCache != null)
497: return;
498:
499: prefsCache = new HashMap<String, String>();
500: try {
501: loadCache();
502: } catch (Exception e) {
503: // assert lastSyncTime == 0;
504: prefsCache = new TreeMap<String, String>();
505: }
506: }
507:
508: /**
509: * Attempt to load prefsCache from the backing store. If the attempt
510: * succeeds, lastSyncTime will be updated (the new value will typically
511: * correspond to the data loaded into the map, but it may be less,
512: * if another VM is updating this node concurrently). If the attempt
513: * fails, a BackingStoreException is thrown and both prefsCache and
514: * lastSyncTime are unaffected by the call.
515: */
516: private void loadCache() throws BackingStoreException {
517:
518: long newLastSyncTime = 0;
519:
520: try {
521:
522: newLastSyncTime = prefsFile.lastModified();
523:
524: DocumentBuilder docBuilder = javax.xml.parsers.DocumentBuilderFactory
525: .newInstance().newDocumentBuilder();
526: org.w3c.dom.Document doc = null;
527:
528: doc = docBuilder.parse(prefsFile);
529:
530: NodeList entries = doc.getElementsByTagName("entry");
531:
532: for (int i = 0; i < entries.getLength(); i++) {
533: Element entry = (Element) entries.item(i);
534:
535: String key = entry.getAttribute("key");
536:
537: String value = entry.getAttribute("value");
538:
539: prefsCache.put(key, value);
540: }
541:
542: } catch (Exception e) {
543: if (e instanceof InvalidPreferencesFormatException) {
544: getLogger().warning(
545: "Invalid preferences format in "
546: + prefsFile.getPath());
547: prefsFile.renameTo(new File(prefsFile.getParentFile(),
548: "IncorrectFormatPrefs.xml"));
549:
550: } else if (e instanceof FileNotFoundException) {
551: getLogger().warning(
552: "Prefs file removed in background "
553: + prefsFile.getPath());
554: } else {
555: throw new BackingStoreException(e);
556: }
557: }
558: // Attempt succeeded; update state
559:
560: lastSyncTime = newLastSyncTime;
561:
562: }
563:
564: /**
565: * Attempt to write back prefsCache to the backing store. If the attempt
566: * succeeds, lastSyncTime will be updated (the new value will correspond
567: * exactly to the data thust written back, as we hold the file lock, which
568: * prevents a concurrent write. If the attempt fails, a
569: * BackingStoreException is thrown and both the backing store (prefsFile)
570: * and lastSyncTime will be unaffected by this call. This call will
571: * NEVER leave prefsFile in a corrupt state.
572: */
573: private void writeBackCache() throws BackingStoreException {
574: OutputStream os;
575: FileOutputStream fos;
576:
577: try {
578: if (!dir.exists() && !dir.mkdirs())
579: throw new BackingStoreException(dir + " create failed.");
580: fos = new FileOutputStream(prefsFile);
581: os = new BufferedOutputStream(fos);
582: //exportSubtree(os);
583:
584: DocumentBuilderFactory f = DocumentBuilderFactory
585: .newInstance();
586: DocumentBuilder build = f.newDocumentBuilder();
587:
588: org.w3c.dom.Document doc = build.newDocument();
589: Element root = doc.createElement("PREFS");
590: doc.appendChild(root);
591:
592: for (String key : prefsCache.keySet()) {
593: Element e = doc.createElement("entry");
594: e.setAttribute("key", key);
595: e.setAttribute("value", prefsCache.get(key));
596: root.appendChild(e);
597:
598: }
599:
600: Transformer trans = TransformerFactory.newInstance()
601: .newTransformer();
602: DOMSource source = new DOMSource(doc);
603: StreamResult result = new StreamResult(os);
604: trans.transform(source, result);
605:
606: os.close();
607: fos.close();
608:
609: } catch (Exception e) {
610: if (e instanceof BackingStoreException)
611: throw (BackingStoreException) e;
612: throw new BackingStoreException(e);
613: }
614:
615: }
616:
617: protected String[] keysSpi() {
618: initCacheIfNecessary();
619: return (String[]) prefsCache.keySet().toArray(
620: new String[prefsCache.size()]);
621: }
622:
623: protected String[] childrenNamesSpi() {
624:
625: List<String> result = new ArrayList<String>();
626: File[] dirContents = dir.listFiles();
627: if (dirContents != null) {
628: for (int i = 0; i < dirContents.length; i++)
629: if (dirContents[i].isDirectory())
630: result.add(dirContents[i].getName());
631: }
632: return result.toArray(EMPTY_STRING_ARRAY);
633:
634: }
635:
636: private static final String[] EMPTY_STRING_ARRAY = new String[0];
637:
638: protected AbstractPreferences childSpi(String name) {
639: return new ServletPreferences(this , name);
640: }
641:
642: public void removeNode() throws BackingStoreException {
643: synchronized (isUserNode() ? userLockFile : systemLockFile) {
644: // to remove a node we need an exclusive lock
645: super .removeNode();
646: }
647: }
648:
649: /**
650: * Called with file lock held (in addition to node locks).
651: */
652: protected void removeNodeSpi() throws BackingStoreException {
653:
654: if (changeLog.contains(nodeCreate)) {
655: changeLog.remove(nodeCreate);
656: nodeCreate = null;
657: return;// null;
658: }
659: if (!dir.exists())
660: return;
661: // return null;
662: prefsFile.delete();
663: tmpFile.delete();
664: // dir should be empty now. If it's not, empty it
665: File[] junk = dir.listFiles();
666: if (junk.length != 0) {
667: getLogger().warning(
668: "Found extraneous files when removing node: "
669: + Arrays.asList(junk));
670: for (int i = 0; i < junk.length; i++) {
671: junk[i].delete();
672: }
673:
674: }
675: if (!dir.delete())
676: throw new BackingStoreException("Couldn't delete dir: "
677: + dir);
678:
679: }
680:
681: public synchronized void sync() throws BackingStoreException {
682:
683: synchronized (isUserNode() ? userLockFile : systemLockFile) {
684: final Long newModTime;
685:
686: if (isUserNode()) {
687: newModTime = userRootModFile.lastModified();
688: isUserRootModified = userRootModTime == newModTime;
689: } else {
690: newModTime = systemRootModFile.lastModified();
691: isSystemRootModified = systemRootModTime == newModTime;
692: }
693:
694: super .sync();
695:
696: if (isUserNode()) {
697: userRootModTime = newModTime.longValue() + 1000;
698: userRootModFile.setLastModified(userRootModTime);
699: } else {
700: systemRootModTime = newModTime.longValue() + 1000;
701: systemRootModFile.setLastModified(systemRootModTime);
702: }
703: }
704: }
705:
706: protected void syncSpi() throws BackingStoreException {
707:
708: syncSpiPrivileged();
709:
710: }
711:
712: private void syncSpiPrivileged() throws BackingStoreException {
713: if (isRemoved())
714: throw new IllegalStateException("Node has been removed");
715: if (prefsCache == null)
716: return; // We've never been used, don't bother syncing
717: long lastModifiedTime;
718: if ((isUserNode() ? isUserRootModified : isSystemRootModified)) {
719: lastModifiedTime = prefsFile.lastModified();
720: if (lastModifiedTime != lastSyncTime) {
721: // Prefs at this node were externally modified; read in node and
722: // playback any local mods since last sync
723: loadCache();
724: replayChanges();
725: lastSyncTime = lastModifiedTime;
726: }
727: } else if (lastSyncTime != 0 && !dir.exists()) {
728: // This node was removed in the background. Playback any changes
729: // against a virgin (empty) Map.
730: prefsCache = new TreeMap<String, String>();
731: replayChanges();
732: }
733: if (!changeLog.isEmpty()) {
734: writeBackCache(); // Creates directory & file if necessary
735: /*
736: * Attempt succeeded; it's barely possible that the call to
737: * lastModified might fail (i.e., return 0), but this would not
738: * be a disaster, as lastSyncTime is allowed to lag.
739: */
740: lastModifiedTime = prefsFile.lastModified();
741: /* If lastSyncTime did not change, or went back
742: * increment by 1 second. Since we hold the lock
743: * lastSyncTime always monotonically encreases in the
744: * atomic sense.
745: */
746: if (lastSyncTime <= lastModifiedTime) {
747: lastSyncTime = lastModifiedTime + 1000;
748: prefsFile.setLastModified(lastSyncTime);
749: }
750: changeLog.clear();
751: }
752: }
753:
754: public void flush() throws BackingStoreException {
755: if (isRemoved())
756: return;
757: sync();
758: }
759:
760: protected void flushSpi() throws BackingStoreException {
761: // assert false;
762: }
763:
764: }
|