001: /*
002: * $Id: PageMap.java 529917 2007-04-18 08:42:01Z jcompagner $ $Revision:
003: * 1.67 $ $Date: 2007-04-18 10:42:01 +0200 (Wed, 18 Apr 2007) $
004: *
005: * ==============================================================================
006: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
007: * use this file except in compliance with the License. You may obtain a copy of
008: * the License at
009: *
010: * http://www.apache.org/licenses/LICENSE-2.0
011: *
012: * Unless required by applicable law or agreed to in writing, software
013: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
014: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
015: * License for the specific language governing permissions and limitations under
016: * the License.
017: */
018: package wicket;
019:
020: import java.io.Serializable;
021: import java.util.ArrayList;
022: import java.util.Iterator;
023: import java.util.List;
024:
025: import org.apache.commons.logging.Log;
026: import org.apache.commons.logging.LogFactory;
027:
028: import wicket.session.pagemap.IPageMapEntry;
029: import wicket.util.collections.ArrayListStack;
030: import wicket.util.lang.Objects;
031:
032: /**
033: * A container for pages held in the session. PageMap is a parameter to several
034: * methods in the Wicket API. You can get a PageMap by name from a Session with
035: * Session.getPageMap(String pageMapName) or more conveniently with
036: * PageMap.forName(String pageMapName). But you should not hold onto a reference
037: * to the pagemap (just as you should not hold onto a reference to your Session
038: * but should get it each time you need it instead). Instead, create a strongly
039: * typed accessor method like this:
040: *
041: * <pre>
042: * public PageMap getMyPageMap()
043: * {
044: * return PageMap.forName("myPageMapName");
045: * }
046: * </pre>
047: *
048: * If the page map with the given name is not found, one will be automatically
049: * created.
050: *
051: * @author Jonathan Locke
052: */
053: public final class PageMap implements Serializable {
054: /** Name of default pagemap */
055: public static final String DEFAULT_NAME = null;
056:
057: /** Log. */
058: private static final Log log = LogFactory.getLog(PageMap.class);
059:
060: private static final long serialVersionUID = 1L;
061:
062: /** Stack of entry accesses by id */
063: private final ArrayListStack/* <Access> */accessStack = new ArrayListStack(
064: 8);
065:
066: /** URL to continue to after a given page. */
067: private String interceptContinuationURL;
068:
069: /** Name of this page map */
070: private final String name;
071:
072: /** Next available page identifier in this page map. */
073: private int pageId = 0;
074:
075: /** The session where this PageMap resides */
076: private transient Session session;
077:
078: /**
079: * Holds information about a pagemap access
080: *
081: * @author Jonathan
082: */
083: public static class Access implements Serializable {
084: private static final long serialVersionUID = 1L;
085:
086: int id;
087: int version;
088:
089: /**
090: * @see java.lang.Object#equals(java.lang.Object)
091: */
092: public boolean equals(Object obj) {
093: if (obj instanceof Access) {
094: Access tmp = (Access) obj;
095: return tmp.id == id && tmp.version == version;
096: }
097: return false;
098: }
099:
100: /**
101: * Gets id.
102: *
103: * @return id
104: */
105: public final int getId() {
106: return id;
107: }
108:
109: /**
110: * Gets version.
111: *
112: * @return version
113: */
114: public final int getVersion() {
115: return version;
116: }
117:
118: /**
119: * @see java.lang.Object#hashCode()
120: */
121: public int hashCode() {
122: return id + (version << 16);
123: }
124:
125: /**
126: * @see java.lang.Object#toString()
127: */
128: public String toString() {
129: return "[Access id=" + id + ", version=" + version + "]";
130: }
131: }
132:
133: /**
134: * Visitor interface for visiting entries in this map
135: *
136: * @author Jonathan Locke
137: */
138: static interface IVisitor {
139: /**
140: * @param entry
141: * The page map entry
142: */
143: public void entry(final IPageMapEntry entry);
144: }
145:
146: /**
147: * Gets a page map for a page map name, automatically creating the page map
148: * if it does not exist. If you do not want the pagemap to be automatically
149: * created, you can call Session.pageMapForName(pageMapName, false).
150: *
151: * @param pageMapName
152: * The name of the page map to get
153: * @return The PageMap with the given name from the current session
154: */
155: public static PageMap forName(final String pageMapName) {
156: Session session = Session.get();
157: return (session != null) ? session.pageMapForName(pageMapName,
158: true) : null;
159: }
160:
161: /**
162: * Constructor
163: *
164: * @param name
165: * The name of this page map
166: * @param session
167: * The session holding this page map
168: */
169: PageMap(final String name, final Session session) {
170: this .name = name;
171: if (session == null) {
172: throw new IllegalArgumentException(
173: "session must be not null");
174: }
175: this .session = session;
176: }
177:
178: /**
179: * Removes all pages from this map
180: */
181: public final void clear() {
182: // Remove all entries
183: visitEntries(new IVisitor() {
184: public void entry(IPageMapEntry entry) {
185: removeEntry(entry);
186: }
187: });
188:
189: // Clear access stack
190: accessStack.clear();
191: }
192:
193: // TODO Post 1.2: We should encode the page id of the current page into the
194: // URL for truly stateless pages so we can adjust the stack correctly
195:
196: /**
197: * Returns a stack of PageMap.Access entries pushed in the order that the
198: * pages and versions were accessed.
199: *
200: * @return Stack containing ids of entries in access order.
201: */
202: public final ArrayListStack getAccessStack() {
203: return accessStack;
204: }
205:
206: /**
207: * Retrieves entry with given id.
208: *
209: * @param id
210: * The page identifier
211: * @return Any entry having the given id
212: */
213: public final IPageMapEntry getEntry(final int id) {
214: return (IPageMapEntry) session.getAttribute(attributeForId(id));
215: }
216:
217: /**
218: * @return Returns the name.
219: */
220: public final String getName() {
221: return name;
222: }
223:
224: /**
225: * @return The session that this PageMap is in.
226: */
227: public final Session getSession() {
228: if (session != Session.get()) {
229: log.error("Session [" + session.getId()
230: + "]in pagemap wasn't the current session ["
231: + Session.get().getId() + "]");
232: session = Session.get();
233: }
234: return session;
235: }
236:
237: /**
238: * @return Size of this page map in bytes, including a sum of the sizes of
239: * all the pages it contains.
240: */
241: public final long getSizeInBytes() {
242: long size = Objects.sizeof(this );
243: for (Iterator iterator = getEntries().iterator(); iterator
244: .hasNext();) {
245: IPageMapEntry entry = (IPageMapEntry) iterator.next();
246: if (entry instanceof Page) {
247: size += ((Page) entry).getSizeInBytes();
248: } else {
249: size += Objects.sizeof(entry);
250: }
251: }
252: return size;
253: }
254:
255: /**
256: * @return Number of page versions stored in this page map
257: */
258: public final int getVersions() {
259: return accessStack.size();
260: }
261:
262: /**
263: * @return True if this is the default page map
264: */
265: public final boolean isDefault() {
266: return name == PageMap.DEFAULT_NAME;
267: }
268:
269: /**
270: * Gets the most recently accessed page map entry off the top of the entry
271: * access stack. This is guaranteed to be the most recently accessed entry
272: * IF AND ONLY IF the user just came from a stateful page. If the user could
273: * get to the current page from a stateless page, this method may not work
274: * if the user uses the back button. For a detailed explanation of this
275: * issue, see getAccessStack().
276: *
277: * @see PageMap#getAccessStack()
278: *
279: * @return Previous pagemap entry in terms of access
280: */
281: public final IPageMapEntry lastAccessedEntry() {
282: return getEntry(peekAccess().getId());
283: }
284:
285: /**
286: * Removes this PageMap from the Session.
287: */
288: public final void remove() {
289: // First clear all pages from the session for this pagemap
290: clear();
291:
292: // Then remove the pagemap itself
293: session.removePageMap(this );
294: }
295:
296: /**
297: * Removes the page from the pagemap
298: *
299: * @param page
300: * page to be removed from the pagemap
301: */
302: public final void remove(final Page page) {
303: // Remove the pagemap entry from session
304: removeEntry(page.getPageMapEntry());
305: }
306:
307: /**
308: * @param entry
309: * The entry to remove
310: */
311: public final void removeEntry(final IPageMapEntry entry) {
312: if (entry == null) {
313: // TODO this shouldn't happen but to many people are still getting this now and then/
314: // so first this "fix"
315: log.warn("PageMap.removeEntry called with an null entry");
316: return;
317: }
318: // Remove entry from session
319: synchronized (session) {
320: session
321: .removeAttribute(attributeForId(entry
322: .getNumericId()));
323:
324: // Remove page from acccess stack
325: final Iterator stack = accessStack.iterator();
326: while (stack.hasNext()) {
327: final Access access = (Access) stack.next();
328: if (access.id == entry.getNumericId()) {
329: stack.remove();
330: }
331: }
332:
333: // Let the session know we changed the pagemap
334: session.dirtyPageMap(this );
335: }
336: }
337:
338: /**
339: * @see java.lang.Object#toString()
340: */
341: public String toString() {
342: return "[PageMap name=" + name + ", access=" + accessStack
343: + "]";
344: }
345:
346: /**
347: * @param id
348: * The page id to create an attribute for
349: * @return The session attribute for the given page (for replication of
350: * state)
351: */
352: final String attributeForId(final int id) {
353: return attributePrefix() + id;
354: }
355:
356: /**
357: * @return The attribute prefix for this page map
358: */
359: final String attributePrefix() {
360: return Session.pageMapEntryAttributePrefix + name + ":";
361: }
362:
363: /**
364: * Redirects to any intercept page previously specified by a call to
365: * redirectToInterceptPage.
366: *
367: * @return True if an original destination was redirected to
368: * @see PageMap#redirectToInterceptPage(Page)
369: */
370: final boolean continueToOriginalDestination() {
371: // Get request cycle
372: final RequestCycle cycle = RequestCycle.get();
373:
374: // If there's a place to go to
375: if (interceptContinuationURL != null) {
376: cycle.setRequestTarget(new IRequestTarget() {
377: final String responseUrl = interceptContinuationURL;
378:
379: public void detach(RequestCycle requestCycle) {
380: }
381:
382: public Object getLock(RequestCycle requestCycle) {
383: return null;
384: }
385:
386: public void respond(RequestCycle requestCycle) {
387: // Redirect there
388: cycle.getResponse().redirect(responseUrl);
389: }
390:
391: });
392:
393: // Reset interception URL
394: interceptContinuationURL = null;
395:
396: // Force session to replicate page maps
397: session.dirtyPageMap(this );
398: return true;
399: }
400: return false;
401: }
402:
403: /**
404: * Retrieves page with given id.
405: *
406: * @param id
407: * The page identifier
408: * @param versionNumber
409: * The version to get
410: * @return Any page having the given id
411: */
412: final Page get(final int id, int versionNumber) {
413: final IPageMapEntry entry = (IPageMapEntry) session
414: .getAttribute(attributeForId(id));
415: if (entry != null) {
416: // Get page as dirty
417: Page page = entry.getPage();
418:
419: // TODO Performance: Is this really the case is a page always dirty
420: // even if we just render it again? POSSIBLE ANSWER: The page could
421: // mark itself as clean to prevent replication, but the reverse is
422: // probably not desirable (pages marking themselves dirty manually)
423: // We ought to think about this a bit and consider whether this
424: // could be tied in with version management. It's only when a page's
425: // version changes that it should be considered dirty, because then
426: // some kind of state changed. Right? - Jonathan
427: page.dirty();
428:
429: // Get the version of the page requested from the page
430: final Page version = page.getVersion(versionNumber);
431:
432: // Entry has been accessed
433: //pushAccess(entry);
434: // Entry has been accessed
435: access(entry, versionOf(entry));
436:
437: // Is the requested version available?
438: if (version != null) {
439: // Need to update session with new page?
440: if (version != page) {
441: // This is our new page
442: page = version;
443:
444: // Replaces old page entry
445: page.getPageMap().put(page);
446: }
447: } else {
448: if (log.isInfoEnabled()) {
449: log.info("Unable to get version " + versionNumber
450: + " of page " + page);
451: }
452: return null;
453: }
454: return page;
455: }
456: return null;
457: }
458:
459: /**
460: * @return The next id for this pagemap
461: */
462: final int nextId() {
463: session.dirtyPageMap(this );
464: return this .pageId++;
465: }
466:
467: /**
468: * @param page
469: * The page to put into this map
470: */
471: final void put(final Page page) {
472: // Page only goes into session if it is stateless
473: if (!page.isStateless()) {
474: // Get page map entry from page
475: final IPageMapEntry entry = page.getPageMapEntry();
476:
477: // Entry has been accessed
478: pushAccess(entry);
479:
480: // Store entry in session
481: final String attribute = attributeForId(entry
482: .getNumericId());
483:
484: if (session.getAttribute(attribute) == null) {
485: // Set attribute if it is a new page, so that it will exists
486: // already for other threads that can come on the same time.
487: session.setAttribute(attribute, entry);
488: } else {
489: // Else don't set it directly but add to the dirty map
490: session.dirtyPage(page);
491: }
492:
493: // Evict any page(s) as need be
494: session.getApplication().getSessionSettings()
495: .getPageMapEvictionStrategy().evict(this );
496: }
497: }
498:
499: /**
500: * Redirects browser to an intermediate page such as a sign-in page. The
501: * current request's URL is saved exactly as it was requested for future use
502: * by continueToOriginalDestination(); Only use this method when you plan to
503: * continue to the current URL at some later time; otherwise just use
504: * setResponsePage or, when you are in a constructor, redirectTo.
505: *
506: * @param page
507: * The page to temporarily redirect to
508: */
509: final void redirectToInterceptPage(final Page page) {
510: // Get the request cycle
511: final RequestCycle cycle = RequestCycle.get();
512:
513: // The intercept continuation URL should be saved exactly as the
514: // original request specified.
515: interceptContinuationURL = cycle.getRequest().getURL();
516:
517: // Page map is dirty
518: session.dirtyPageMap(this );
519:
520: // Redirect to the page
521: cycle.setRedirect(true);
522: cycle.setResponsePage(page);
523: }
524:
525: /**
526: * Redirects browser to an intermediate page such as a sign-in page. The
527: * current request's URL is saved exactly as it was requested for future use
528: * by continueToOriginalDestination(); Only use this method when you plan to
529: * continue to the current URL at some later time; otherwise just use
530: * setResponsePage or, when you are in a constructor, redirectTo.
531: *
532: * @param pageClazz
533: * The page clazz to temporarily redirect to
534: */
535: final void redirectToInterceptPage(final Class pageClazz) {
536: // Get the request cycle
537: final RequestCycle cycle = RequestCycle.get();
538:
539: // The intercept continuation URL should be saved exactly as the
540: // original request specified.
541: interceptContinuationURL = cycle.getRequest().getURL();
542:
543: // Page map is dirty
544: session.dirtyPageMap(this );
545:
546: // Redirect to the page
547: cycle.setRedirect(true);
548: cycle.setResponsePage(pageClazz);
549: }
550:
551: /**
552: * @param session
553: * Session to set
554: */
555: final void setSession(final Session session) {
556: this .session = session;
557: }
558:
559: /**
560: * @param visitor
561: * The visitor to call at each Page in this PageMap.
562: */
563: final void visitEntries(final IVisitor visitor) {
564: final List attributes = session.getAttributeNames();
565: for (final Iterator iterator = attributes.iterator(); iterator
566: .hasNext();) {
567: final String attribute = (String) iterator.next();
568: if (attribute.startsWith(attributePrefix())) {
569: visitor.entry((IPageMapEntry) session
570: .getAttribute(attribute));
571: }
572: }
573: }
574:
575: /**
576: * @param entry
577: * Add entry to access list
578: * @param version
579: * Version number being accessed
580: */
581: private final void access(final IPageMapEntry entry,
582: final int version) {
583: // See if the version being accessed is already in the stack
584: boolean add = true;
585: int id = entry.getNumericId();
586: for (int i = accessStack.size() - 1; i >= 0; i--) {
587: final Access access = (Access) accessStack.get(i);
588:
589: // If we found id and version in access stack
590: if (access.id == id && access.version == version) {
591: // No need to add since id and version are already in stack
592: add = false;
593:
594: // Pop entries to reveal that version at top of stack
595: // because the user used the back button
596: while (i < accessStack.size() - 1) {
597: // Pop unreachable access off top of stack
598: final Access topAccess = popAccess();
599:
600: // Get entry for access
601: final IPageMapEntry top = getEntry(topAccess
602: .getId());
603:
604: // If it's a page we can remove version info
605: if (top instanceof Page) {
606: // If there's more than one version
607: Page topPage = (Page) top;
608: if (topPage.getVersions() > 1) {
609: // Remove version the top access version (-1)
610: topPage
611: .getVersion(topAccess.getVersion() - 1);
612: } else {
613: // Remove whole page
614: remove(topPage);
615: }
616: } else {
617: // Remove entry
618: removeEntry(top);
619: }
620: }
621: break;
622: }
623: }
624:
625: // If the user did not use the back button
626: if (add) {
627: pushAccess(entry);
628: }
629: }
630:
631: /**
632: * @return List of entries in this page map
633: */
634: private final List getEntries() {
635: final List attributes = session.getAttributeNames();
636: final List list = new ArrayList();
637: for (final Iterator iterator = attributes.iterator(); iterator
638: .hasNext();) {
639: final String attribute = (String) iterator.next();
640: if (attribute.startsWith(attributePrefix())) {
641: list.add(session.getAttribute(attribute));
642: }
643: }
644: return list;
645: }
646:
647: /**
648: * @return Access entry on top of the access stack
649: */
650: private final Access peekAccess() {
651: return (Access) accessStack.peek();
652: }
653:
654: /**
655: * Removes access entry on top of stack
656: *
657: * @return Access entry on top of the access stack
658: */
659: private final Access popAccess() {
660: session.dirtyPageMap(this );
661: return (Access) accessStack.pop();
662: }
663:
664: /**
665: * @param entry
666: * Entry that was accessed
667: */
668: private final void pushAccess(IPageMapEntry entry) {
669: // Create new access entry
670: final Access access = new Access();
671: access.id = entry.getNumericId();
672: access.version = versionOf(entry);
673: if (accessStack.size() > 0) {
674: if (peekAccess().equals(access)) {
675: return;
676: }
677: int index = accessStack.indexOf(access);
678: if (index >= 0) {
679: accessStack.remove(index);
680: }
681: }
682: accessStack.push(access);
683: session.dirtyPageMap(this );
684: }
685:
686: /**
687: * @param entry
688: * Page map entry
689: * @return Version of entry
690: */
691: private final int versionOf(final IPageMapEntry entry) {
692: if (entry instanceof Page) {
693: return ((Page) entry).getCurrentVersionNumber();
694: }
695:
696: // If entry is not a page, it cannot have versions because the Page
697: // is constructed on the fly.
698: return 0;
699: }
700: }
|