0001: /*
0002: * Licensed to the Apache Software Foundation (ASF) under one or more
0003: * contributor license agreements. See the NOTICE file distributed with
0004: * this work for additional information regarding copyright ownership.
0005: * The ASF licenses this file to You under the Apache License, Version 2.0
0006: * (the "License"); you may not use this file except in compliance with
0007: * the License. You may obtain a copy of the License at
0008: *
0009: * http://www.apache.org/licenses/LICENSE-2.0
0010: *
0011: * Unless required by applicable law or agreed to in writing, software
0012: * distributed under the License is distributed on an "AS IS" BASIS,
0013: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
0014: * See the License for the specific language governing permissions and
0015: * limitations under the License.
0016: */
0017: package org.apache.wicket;
0018:
0019: import java.io.IOException;
0020: import java.io.ObjectInputStream;
0021: import java.io.ObjectOutputStream;
0022: import java.io.ObjectStreamException;
0023: import java.util.ArrayList;
0024: import java.util.HashSet;
0025: import java.util.List;
0026: import java.util.Set;
0027:
0028: import org.apache.wicket.authorization.IAuthorizationStrategy;
0029: import org.apache.wicket.authorization.UnauthorizedActionException;
0030: import org.apache.wicket.authorization.strategies.page.SimplePageAuthorizationStrategy;
0031: import org.apache.wicket.feedback.IFeedback;
0032: import org.apache.wicket.markup.MarkupException;
0033: import org.apache.wicket.markup.MarkupStream;
0034: import org.apache.wicket.markup.html.WebPage;
0035: import org.apache.wicket.markup.html.form.Form;
0036: import org.apache.wicket.model.IModel;
0037: import org.apache.wicket.request.RequestParameters;
0038: import org.apache.wicket.session.pagemap.IPageMapEntry;
0039: import org.apache.wicket.settings.IDebugSettings;
0040: import org.apache.wicket.util.concurrent.ConcurrentHashMap;
0041: import org.apache.wicket.util.lang.Classes;
0042: import org.apache.wicket.util.lang.Objects;
0043: import org.apache.wicket.util.string.StringValue;
0044: import org.apache.wicket.util.value.Count;
0045: import org.apache.wicket.version.IPageVersionManager;
0046: import org.apache.wicket.version.undo.Change;
0047: import org.slf4j.Logger;
0048: import org.slf4j.LoggerFactory;
0049:
0050: /**
0051: * Abstract base class for pages. As a MarkupContainer subclass, a Page can
0052: * contain a component hierarchy and markup in some markup language such as
0053: * HTML. Users of the framework should not attempt to subclass Page directly.
0054: * Instead they should subclass a subclass of Page that is appropriate to the
0055: * markup type they are using, such as WebPage (for HTML markup).
0056: * <ul>
0057: * <li><b>Construction </b>- When a page is constructed, it is automatically
0058: * added to the current PageMap in the Session. When a Page is added to the
0059: * Session's PageMap, the PageMap assigns the Page an id. A PageMap is roughly
0060: * equivalent to a browser window and encapsulates a set of pages accessible
0061: * through that window. When a popup window is created, a new PageMap is created
0062: * for the popup.
0063: *
0064: * <li><b>Identity </b>- The Session that a Page is contained in can be
0065: * retrieved by calling Page.getSession(). Page identifiers start at 0 for each
0066: * PageMap in the Session and increment as new pages are added to the map. The
0067: * PageMap-(and Session)-unique identifier assigned to a given Page can be
0068: * retrieved by calling getId(). So, the first Page added to a new user Session
0069: * will always be named "0".
0070: *
0071: * <li><b>LifeCycle </b>- Subclasses of Page which are interested in lifecycle
0072: * events can override onBeginRequest, onEndRequest() and onModelChanged(). The
0073: * onBeginRequest() method is inherited from Component. A call to
0074: * onBeginRequest() is made for every Component on a Page before page rendering
0075: * begins. At the end of a request (when rendering has completed) to a Page, the
0076: * onEndRequest() method is called for every Component on the Page.
0077: *
0078: * <li><b>Nested Component Hierarchy </b>- The Page class is a subclass of
0079: * MarkupContainer. All MarkupContainers can have "associated markup", which
0080: * resides alongside the Java code by default. All MarkupContainers are also
0081: * Component containers. Through nesting, of containers, a Page can contain any
0082: * arbitrary tree of Components. For more details on MarkupContainers, see
0083: * {@link org.apache.wicket.MarkupContainer}.
0084: *
0085: * <li><b>Bookmarkable Pages </b>- Pages can be constructed with any
0086: * constructor when they are being used in a Wicket session, but if you wish to
0087: * link to a Page using a URL that is "bookmarkable" (which implies that the URL
0088: * will not have any session information encoded in it, and that you can call
0089: * this page directly without having a session first directly from your
0090: * browser), you need to implement your Page with a no-arg constructor or with a
0091: * constructor that accepts a PageParameters argument (which wraps any query
0092: * string parameters for a request). In case the page has both constructors, the
0093: * constructor with PageParameters will be used.
0094: *
0095: * <li><b>Models </b>- Pages, like other Components, can have models (see
0096: * {@link IModel}). A Page can be assigned a model by passing one to the Page's
0097: * constructor, by overriding initModel() or with an explicit invocation of
0098: * setModel(). If the model is a
0099: * {@link org.apache.wicket.model.CompoundPropertyModel}, Components on the
0100: * Page can use the Page's model implicitly via container inheritance. If a
0101: * Component is not assigned a model, the initModel() override in Component will
0102: * cause that Component to use the nearest CompoundModel in the parent chain, in
0103: * this case, the Page's model. For basic CompoundModels, the name of the
0104: * Component determines which property of the implicit page model the component
0105: * is bound to. If more control is desired over the binding of Components to the
0106: * page model (for example, if you want to specify some property expression
0107: * other than the component's name for retrieving the model object),
0108: * BoundCompoundPropertyModel can be used.
0109: *
0110: * <li><b>Back Button </b>- Pages can support the back button by enabling
0111: * versioning with a call to setVersioned(boolean). If a Page is versioned and
0112: * changes occur to it which need to be tracked, a verison manager will be
0113: * installed using the overridable factory method newVersionManager(). The
0114: * default version manager returned by the base implementation of this method is
0115: * an instance of UndoPageVersionManager, which manages versions of a page by
0116: * keeping change records that can be reversed at a later time.
0117: *
0118: * <li><b>Security </b>- See {@link IAuthorizationStrategy},
0119: * {@link SimplePageAuthorizationStrategy}
0120: *
0121: * @see org.apache.wicket.markup.html.WebPage
0122: * @see org.apache.wicket.MarkupContainer
0123: * @see org.apache.wicket.model.CompoundPropertyModel
0124: * @see org.apache.wicket.model.BoundCompoundPropertyModel
0125: * @see org.apache.wicket.Component
0126: * @see org.apache.wicket.version.IPageVersionManager
0127: * @see org.apache.wicket.version.undo.UndoPageVersionManager
0128: *
0129: * @author Jonathan Locke
0130: * @author Chris Turner
0131: * @author Eelco Hillenius
0132: * @author Johan Compagner
0133: */
0134: public abstract class Page extends MarkupContainer implements
0135: IRedirectListener, IPageMapEntry {
0136: /**
0137: * You can set implementation of the interface in the
0138: * {@link Page#serializer} then that implementation will handle the
0139: * serialization of this page. The serializePage method is called from the
0140: * writeObject method then the implementation override the default
0141: * serialization.
0142: *
0143: * @author jcompagner
0144: */
0145: public static interface IPageSerializer {
0146: /**
0147: * Called when page is being deserialized
0148: *
0149: * @param id
0150: * TODO
0151: * @param name
0152: * TODO
0153: * @param page
0154: * @param stream
0155: * @return New instance to replace page instance being deserialized
0156: * @throws IOException
0157: * @throws ClassNotFoundException
0158: *
0159: */
0160: public Page deserializePage(int id, String name, Page page,
0161: ObjectInputStream stream) throws IOException,
0162: ClassNotFoundException;
0163:
0164: /**
0165: * Called from the {@link Page#writeObject()} method.
0166: *
0167: * @param page
0168: * The page that must be serialized.
0169: * @param stream
0170: * ObjectOutputStream
0171: * @throws IOException
0172: */
0173: public void serializePage(Page page, ObjectOutputStream stream)
0174: throws IOException;
0175: }
0176:
0177: /**
0178: * When passed to {@link Page#getVersion(int)} the latest page version is
0179: * returned.
0180: */
0181: public static final int LATEST_VERSION = -1;
0182:
0183: /**
0184: * This is a thread local that is used for serializing page references in
0185: * this page.It stores a {@link IPageSerializer} which can be set by the
0186: * outside world to do the serialization of this page.
0187: */
0188: public static final ThreadLocal serializer = new ThreadLocal();
0189:
0190: /** True if a new version was created for this request. */
0191: private static final short FLAG_NEW_VERSION = FLAG_RESERVED3;
0192:
0193: /** True if the page should try to be stateless */
0194: private static final int FLAG_STATELESS_HINT = FLAG_RESERVED5;
0195:
0196: /** True if component changes are being tracked. */
0197: private static final short FLAG_TRACK_CHANGES = FLAG_RESERVED4;
0198:
0199: /** Log. */
0200: private static final Logger log = LoggerFactory
0201: .getLogger(Page.class);
0202:
0203: /**
0204: * {@link #isBookmarkable()} is expensive, we cache the result here
0205: */
0206: private static final ConcurrentHashMap pageClassToBookmarkableCache = new ConcurrentHashMap();
0207:
0208: private static final long serialVersionUID = 1L;
0209:
0210: /** Used to create page-unique numbers */
0211: private short autoIndex;
0212:
0213: /** Numeric version of this page's id */
0214: private int numericId;
0215:
0216: /** The PageMap within the session that this page is stored in */
0217: private transient IPageMap pageMap;
0218:
0219: /** Name of PageMap that this page is stored in */
0220: private String pageMapName;
0221:
0222: // temporary variable to pass page instance from readObject to readResolve
0223: private transient Page pageToResolve = null;
0224:
0225: /** Set of components that rendered if component use checking is enabled */
0226: private transient Set renderedComponents;
0227:
0228: /**
0229: * Boolean if the page is stateless, so it doesn't have to be in the page
0230: * map, will be set in urlFor
0231: */
0232: private transient Boolean stateless = null;
0233:
0234: /** Version manager for this page */
0235: private IPageVersionManager versionManager;
0236:
0237: /**
0238: * Constructor.
0239: */
0240: protected Page() {
0241: // A Page's id is not determined until setId is called when the Page is
0242: // added to a PageMap in the Session.
0243: super (null);
0244: init();
0245: }
0246:
0247: /**
0248: * Constructor.
0249: *
0250: * @param model
0251: * See Component
0252: * @see Component#Component(String, IModel)
0253: */
0254: protected Page(final IModel model) {
0255: // A Page's id is not determined until setId is called when the Page is
0256: // added to a PageMap in the Session.
0257: super (null, model);
0258: init();
0259: }
0260:
0261: /**
0262: * Constructor.
0263: *
0264: * @param pageMap
0265: * The page map to put this page in
0266: */
0267: protected Page(final IPageMap pageMap) {
0268: // A Page's id is not determined until setId is called when the Page is
0269: // added to a PageMap in the Session.
0270: super (null);
0271: init(pageMap);
0272: }
0273:
0274: /**
0275: * Constructor.
0276: *
0277: * @param pageMap
0278: * the name of the page map to put this page in
0279: * @param model
0280: * See Component
0281: * @see Component#Component(String, IModel)
0282: */
0283: protected Page(final IPageMap pageMap, final IModel model) {
0284: // A Page's id is not determined until setId is called when the Page is
0285: // added to a PageMap in the Session.
0286: super (null, model);
0287: init(pageMap);
0288: }
0289:
0290: /**
0291: * Constructor.
0292: *
0293: * @param parameters
0294: * externally passed parameters
0295: * @see PageParameters
0296: */
0297: protected Page(final PageParameters parameters) {
0298: super (null);
0299: init();
0300: }
0301:
0302: /**
0303: * Called right after a component's listener method (the provided method
0304: * argument) was called. This method may be used to clean up dependencies,
0305: * do logging, etc. NOTE: this method will also be called when
0306: * {@link WebPage#beforeCallComponent(Component, RequestListenerInterface)}
0307: * or the method invocation itself failed.
0308: *
0309: * @param component
0310: * the component that is to be called
0311: * @param listener
0312: * the listener of that component that is to be called
0313: */
0314: // TODO Post-1.3: We should create a listener on Application like
0315: // IComponentInstantiationListener
0316: // that forwards to IAuthorizationStrategy for RequestListenerInterface
0317: // invocations.
0318: public void afterCallComponent(final Component component,
0319: final RequestListenerInterface listener) {
0320: }
0321:
0322: /**
0323: * Called just before a component's listener method (the provided method
0324: * argument) is called. This method may be used to set up dependencies,
0325: * enforce authorization, etc. NOTE: if this method fails, the method will
0326: * not be excuted. Method
0327: * {@link WebPage#afterCallComponent(Component, RequestListenerInterface)}
0328: * will always be called.
0329: *
0330: * @param component
0331: * the component that is to be called
0332: * @param listener
0333: * the listener of that component that is to be called
0334: */
0335: // TODO Post-1.3: We should create a listener on Application like
0336: // IComponentInstantiationListener
0337: // that forwards to IAuthorizationStrategy for RequestListenerInterface
0338: // invocations.
0339: public void beforeCallComponent(final Component component,
0340: final RequestListenerInterface listener) {
0341: }
0342:
0343: /**
0344: * Adds a component to the set of rendered components.
0345: *
0346: * @param component
0347: * The component that was rendered
0348: */
0349: public final void componentRendered(final Component component) {
0350: // Inform the page that this component rendered
0351: if (Application.get().getDebugSettings().getComponentUseCheck()) {
0352: if (renderedComponents == null) {
0353: renderedComponents = new HashSet();
0354: }
0355: if (renderedComponents.add(component) == false) {
0356: throw new MarkupException(
0357: "The component "
0358: + component
0359: + " has the same wicket:id as another component already added at the same level");
0360: }
0361: if (log.isDebugEnabled()) {
0362: log.debug("Rendered " + component);
0363: }
0364: }
0365: }
0366:
0367: /**
0368: * Detaches any attached models referenced by this page.
0369: */
0370: public void detachModels() {
0371: // // visit all this page's children to detach the models
0372: // visitChildren(new IVisitor()
0373: // {
0374: // public Object component(Component component)
0375: // {
0376: // try
0377: // {
0378: // // detach any models of the component
0379: // component.detachModels();
0380: // }
0381: // catch (Exception e) // catch anything; we MUST detach all models
0382: // {
0383: // log.error("detaching models of component " + component + " failed:",
0384: // e);
0385: // }
0386: // return IVisitor.CONTINUE_TRAVERSAL;
0387: // }
0388: // });
0389:
0390: super .detachModels();
0391: }
0392:
0393: /**
0394: * Mark this page as dirty in the session
0395: */
0396: public final void dirty() {
0397: Session.get().dirtyPage(this );
0398: }
0399:
0400: /**
0401: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL.
0402: *
0403: * This method is called when a component was rendered standalone. If it is
0404: * a markupcontainer then the rendering for that container is checked.
0405: *
0406: * @param component
0407: *
0408: */
0409: public final void endComponentRender(Component component) {
0410: if (component instanceof MarkupContainer) {
0411: checkRendering((MarkupContainer) component);
0412: } else {
0413: renderedComponents = null;
0414: }
0415: }
0416:
0417: /**
0418: * Expire the oldest version of this page
0419: */
0420: public final void expireOldestVersion() {
0421: if (versionManager != null) {
0422: versionManager.expireOldestVersion();
0423: }
0424: }
0425:
0426: /**
0427: * @return The current ajax version number of this page.
0428: */
0429: public final int getAjaxVersionNumber() {
0430: return versionManager == null ? 0 : versionManager
0431: .getAjaxVersionNumber();
0432: }
0433:
0434: /**
0435: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
0436: *
0437: * Get a page unique number, which will be increased with each call.
0438: *
0439: * @return A page unique number
0440: */
0441: public final short getAutoIndex() {
0442: return autoIndex++;
0443: }
0444:
0445: /**
0446: * @return The current version number of this page. If the page has been
0447: * changed once, the return value will be 1. If the page has not yet
0448: * been revised, the version returned will be 0, indicating that the
0449: * page is still in its original state.
0450: */
0451: public final int getCurrentVersionNumber() {
0452: return versionManager == null ? 0 : versionManager
0453: .getCurrentVersionNumber();
0454: }
0455:
0456: /**
0457: * @see org.apache.wicket.Component#getId()
0458: */
0459: public final String getId() {
0460: return Integer.toString(numericId);
0461: }
0462:
0463: /**
0464: * @see org.apache.wicket.session.pagemap.IPageMapEntry#getNumericId()
0465: */
0466: public int getNumericId() {
0467: return numericId;
0468: }
0469:
0470: /**
0471: * @see org.apache.wicket.session.pagemap.IPageMapEntry#getPageClass()
0472: */
0473: public final Class getPageClass() {
0474: return getClass();
0475: }
0476:
0477: /**
0478: * @return Returns the PageMap that this Page is stored in.
0479: */
0480: public final IPageMap getPageMap() {
0481: // If the transient needs to be restored
0482: if (pageMap == null) {
0483: // Look the page map up in the session
0484: pageMap = PageMap.forName(pageMapName);
0485: }
0486: return pageMap;
0487: }
0488:
0489: /**
0490: * @return Get a page map entry for this page. By default, this is the page
0491: * itself. But if you know of some way to compress the state for the
0492: * page, you can return a custom implementation that produces the
0493: * page on-the-fly.
0494: */
0495: public IPageMapEntry getPageMapEntry() {
0496: return this ;
0497: }
0498:
0499: /**
0500: * @return
0501: */
0502: public final String getPageMapName() {
0503: return pageMapName;
0504: }
0505:
0506: /**
0507: * @return Size of this page in bytes
0508: */
0509: public final long getSizeInBytes() {
0510: pageMap = null;
0511: return Objects.sizeof(this );
0512: }
0513:
0514: /**
0515: * Returns whether the page should try to be stateless. To be stateless,
0516: * getStatelessHint() of every component on page (and it's behavior) must
0517: * return true and the page must be bookmarkable.
0518: *
0519: * @see org.apache.wicket.Component#getStatelessHint()
0520: */
0521: public final boolean getStatelessHint() {
0522: return getFlag(FLAG_STATELESS_HINT);
0523: }
0524:
0525: /**
0526: * Override this method to implement a custom way of producing a version of
0527: * a Page when it cannot be found in the Session.
0528: *
0529: * @param versionNumber
0530: * The version desired
0531: * @return A Page object with the component/model hierarchy that was
0532: * attached to this page at the time represented by the requested
0533: * version.
0534: */
0535: public Page getVersion(final int versionNumber) {
0536: // If we're still the original Page and that's what's desired
0537: if (versionManager == null) {
0538: if (versionNumber == 0 || versionNumber == LATEST_VERSION) {
0539: return this ;
0540: } else {
0541: log
0542: .info("No version manager available to retrieve requested versionNumber "
0543: + versionNumber);
0544: return null;
0545: }
0546: } else {
0547: // Save original change tracking state
0548: final boolean originalTrackChanges = getFlag(FLAG_TRACK_CHANGES);
0549:
0550: try {
0551: // While the version manager is potentially playing around with
0552: // the Page, it may change the page in order to undo changes and
0553: // we don't want change tracking going on while its doing this.
0554: setFlag(FLAG_TRACK_CHANGES, false);
0555:
0556: // Get page of desired version
0557: final Page page;
0558: if (versionNumber != LATEST_VERSION) {
0559: page = versionManager.getVersion(versionNumber);
0560: } else {
0561: page = versionManager
0562: .getVersion(getCurrentVersionNumber());
0563: }
0564:
0565: // If we went all the way back to the original page
0566: if (page != null && page.getCurrentVersionNumber() == 0
0567: && page.getAjaxVersionNumber() == 0) {
0568: // remove version info
0569: page.versionManager = null;
0570: }
0571:
0572: return page;
0573: } finally {
0574: // Restore change tracking state
0575: setFlag(FLAG_TRACK_CHANGES, originalTrackChanges);
0576: }
0577: }
0578: }
0579:
0580: /**
0581: * @return Number of versions of this page
0582: */
0583: public final int getVersions() {
0584: return versionManager == null ? 1 : versionManager
0585: .getVersions() + 1;
0586: }
0587:
0588: /**
0589: * @return This page's component hierarchy as a string
0590: */
0591: public final String hierarchyAsString() {
0592: final StringBuffer buffer = new StringBuffer();
0593: buffer.append("Page " + getId() + " (version "
0594: + getCurrentVersionNumber() + ")");
0595: visitChildren(new IVisitor() {
0596: public Object component(Component component) {
0597: int levels = 0;
0598: for (Component current = component; current != null; current = current
0599: .getParent()) {
0600: levels++;
0601: }
0602: buffer.append(StringValue.repeat(levels, " ")
0603: + component.getPageRelativePath() + ":"
0604: + Classes.simpleName(component.getClass()));
0605: return null;
0606: }
0607: });
0608: return buffer.toString();
0609: }
0610:
0611: /**
0612: * Call this method when the current (ajax) request shouldn't merge the
0613: * changes that are happening to the page with the previous version.
0614: *
0615: * This is for example needed when you want to redirect to this page in an
0616: * ajax request and then you do want to version normally..
0617: *
0618: * This method doesn't do anything if the getRequest().mergeVersion doesn't
0619: * return true.
0620: */
0621: public final void ignoreVersionMerge() {
0622: if (getRequest().mergeVersion()) {
0623: mayTrackChangesFor(this , null);
0624: if (versionManager != null) {
0625: versionManager.ignoreVersionMerge();
0626: }
0627: }
0628: }
0629:
0630: /**
0631: * Bookmarkable page can be instantiated using a bookmarkable URL.
0632: *
0633: * @return Returns true if the page is bookmarkable.
0634: */
0635: public boolean isBookmarkable() {
0636: Boolean bookmarkable = (Boolean) pageClassToBookmarkableCache
0637: .get(getClass().getName());
0638: if (bookmarkable == null) {
0639: try {
0640:
0641: if (getClass().getConstructor(new Class[] {}) != null) {
0642: bookmarkable = Boolean.TRUE;
0643: }
0644:
0645: } catch (Exception ignore) {
0646: try {
0647: if (getClass().getConstructor(
0648: new Class[] { PageParameters.class }) != null) {
0649: bookmarkable = Boolean.TRUE;
0650: }
0651: } catch (Exception ignore2) {
0652: }
0653: }
0654: if (bookmarkable == null) {
0655: bookmarkable = Boolean.FALSE;
0656: }
0657: pageClassToBookmarkableCache.put(getClass().getName(),
0658: bookmarkable);
0659: }
0660: return bookmarkable.booleanValue();
0661:
0662: }
0663:
0664: /**
0665: * Override this method and return true if your page is used to display
0666: * Wicket errors. This can help the framework prevent infinite failure
0667: * loops.
0668: *
0669: * @return True if this page is intended to display an error to the end
0670: * user.
0671: */
0672: public boolean isErrorPage() {
0673: return false;
0674: }
0675:
0676: /**
0677: * Gets whether the page is stateless. Components on stateless page must not
0678: * render any statefull urls, and components on statefull page must not
0679: * render any stateless urls. Statefull urls are urls, which refer to a
0680: * certain (current) page instance.
0681: *
0682: * @return Whether to page is stateless
0683: */
0684: public final boolean isPageStateless() {
0685: if (isBookmarkable() == false) {
0686: stateless = Boolean.FALSE;
0687: if (getStatelessHint()) {
0688: log
0689: .warn("Page '"
0690: + this
0691: + "' is not stateless because it is not bookmarkable, "
0692: + "but the stateless hint is set to true!");
0693: }
0694: }
0695:
0696: if (stateless == null) {
0697: final Object[] returnArray = new Object[1];
0698: Object returnValue = visitChildren(Component.class,
0699: new IVisitor() {
0700: public Object component(Component component) {
0701: if (!component.isStateless()) {
0702: returnArray[0] = component;
0703: return Boolean.FALSE;
0704: }
0705:
0706: return CONTINUE_TRAVERSAL;
0707: }
0708: });
0709: if (returnValue == null) {
0710: stateless = Boolean.TRUE;
0711: } else if (returnValue instanceof Boolean) {
0712: stateless = (Boolean) returnValue;
0713: }
0714:
0715: if (!stateless.booleanValue() && getStatelessHint()) {
0716: log.warn("Page '" + this
0717: + "' is not stateless because of '"
0718: + returnArray[0]
0719: + "' but the stateless hint is set to true!");
0720: }
0721: }
0722:
0723: return stateless.booleanValue();
0724: }
0725:
0726: /**
0727: * Redirect to this page.
0728: *
0729: * @see org.apache.wicket.IRedirectListener#onRedirect()
0730: */
0731: public final void onRedirect() {
0732: }
0733:
0734: /**
0735: * Convenience method. Search for children of type fromClass and invoke
0736: * their respective removePersistedFormData() methods.
0737: *
0738: * @see Form#removePersistentFormComponentValues(boolean)
0739: *
0740: * @param formClass
0741: * Form to be selected. Pages may have more than one Form.
0742: * @param disablePersistence
0743: * if true, disable persistence for all FormComponents on that
0744: * page. If false, it will remain unchanged.
0745: */
0746: public final void removePersistedFormData(final Class formClass,
0747: final boolean disablePersistence) {
0748: // Check that formClass is an instanceof Form
0749: if (!Form.class.isAssignableFrom(formClass)) {
0750: throw new WicketRuntimeException("Form class "
0751: + formClass.getName()
0752: + " is not a subclass of Form");
0753: }
0754:
0755: // Visit all children which are an instance of formClass
0756: visitChildren(formClass, new IVisitor() {
0757: public Object component(final Component component) {
0758: // They must be of type Form as well
0759: if (component instanceof Form) {
0760: // Delete persistet FormComponent data and disable
0761: // persistence
0762: ((Form) component)
0763: .removePersistentFormComponentValues(disablePersistence);
0764: }
0765: return CONTINUE_TRAVERSAL;
0766: }
0767: });
0768: }
0769:
0770: /**
0771: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT.
0772: */
0773: public final void renderPage() {
0774: // if not attached then attach the whole page.
0775: if (!isAttached()) {
0776: attach();
0777: }
0778: // first try to check if the page can be rendered:
0779: if (!isActionAuthorized(RENDER)) {
0780: if (log.isDebugEnabled()) {
0781: log.debug("Page not allowed to render: " + this );
0782: }
0783: throw new UnauthorizedActionException(this ,
0784: Component.RENDER);
0785: }
0786:
0787: // Make sure it is really empty
0788: renderedComponents = null;
0789:
0790: // Reset it to stateless so that it can be tested again
0791: stateless = null;
0792:
0793: // Set form component values from cookies
0794: setFormComponentValuesFromCookies();
0795:
0796: // First, give priority to IFeedback instances, as they have to
0797: // collect their messages before components like ListViews
0798: // remove any child components
0799: visitChildren(IFeedback.class, new IVisitor() {
0800: public Object component(Component component) {
0801: ((IFeedback) component).updateFeedback();
0802: return IVisitor.CONTINUE_TRAVERSAL;
0803: }
0804: });
0805:
0806: if (this instanceof IFeedback) {
0807: ((IFeedback) this ).updateFeedback();
0808: }
0809:
0810: try {
0811: prepareForRender();
0812: } catch (RuntimeException e) {
0813: // if an exception is thrown then we have to call after render
0814: // else the components could be in a wrong state (rendering)
0815: try {
0816: afterRender();
0817: } catch (RuntimeException e2) {
0818: // ignore this one could be a result off.
0819: }
0820: throw e;
0821: }
0822:
0823: // Visit all this page's children to reset markup streams and check
0824: // rendering authorization, as appropriate. We set any result; positive
0825: // or negative as a temporary boolean in the components, and when a
0826: // authorization exception is thrown it will block the rendering of this
0827: // page
0828:
0829: // first the page itself
0830: setRenderAllowed(isActionAuthorized(RENDER));
0831: // children of the page
0832: visitChildren(new IVisitor() {
0833: public Object component(final Component component) {
0834: // Find out if this component can be rendered
0835: final boolean renderAllowed = component
0836: .isActionAuthorized(RENDER);
0837:
0838: // Authorize rendering
0839: component.setRenderAllowed(renderAllowed);
0840: return IVisitor.CONTINUE_TRAVERSAL;
0841: }
0842: });
0843:
0844: // Handle request by rendering page
0845: try {
0846: render(null);
0847: } finally {
0848: afterRender();
0849: }
0850:
0851: // Check rendering if it happened fully
0852: checkRendering(this );
0853:
0854: // clean up debug meta data if component check is on
0855: if (Application.get().getDebugSettings().getComponentUseCheck()) {
0856: visitChildren(new IVisitor() {
0857: public Object component(Component component) {
0858: component.setMetaData(Component.CONSTRUCTED_AT_KEY,
0859: null);
0860: component.setMetaData(Component.ADDED_AT_KEY, null);
0861: return CONTINUE_TRAVERSAL;
0862: }
0863: });
0864: }
0865:
0866: if (!isPageStateless()) {
0867: // trigger creation of the actual session in case it was deferred
0868: Session.get().getSessionStore().getSessionId(
0869: RequestCycle.get().getRequest(), true);
0870: // Add/touch the response page in the session (its pagemap).
0871: getSession().touch(this );
0872: }
0873: }
0874:
0875: /**
0876: * This returns a page instance that is rollbacked the number of versions
0877: * that is specified compared to the current page.
0878: *
0879: * This is a rollback including ajax versions.
0880: *
0881: * @param numberOfVersions
0882: * to rollback
0883: * @return rollbacked page instance
0884: */
0885: public final Page rollbackPage(int numberOfVersions) {
0886: Page page = versionManager == null ? this : versionManager
0887: .rollbackPage(numberOfVersions);
0888: getSession().touch(page);
0889: return page;
0890: }
0891:
0892: /**
0893: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL.
0894: *
0895: * Set the id for this Page. This method is called by PageMap when a Page is
0896: * added because the id, which is assigned by PageMap, is not known until
0897: * this time.
0898: *
0899: * @param id
0900: * The id
0901: */
0902: public final void setNumericId(final int id) {
0903: numericId = id;
0904: }
0905:
0906: /**
0907: * Sets whether the page should try to be stateless. To be stateless,
0908: * getStatelessHint() of every component on page (and it's behavior) must
0909: * return true and the page must be bookmarkable.
0910: *
0911: * @param value
0912: * whether the page should try to be stateless
0913: */
0914: public final void setStatelessHint(boolean value) {
0915: if (value && !isBookmarkable()) {
0916: throw new WicketRuntimeException(
0917: "Can't set stateless hint to true on a page when the page is not bookmarkable, page: "
0918: + this );
0919: }
0920: setFlag(FLAG_STATELESS_HINT, value);
0921: }
0922:
0923: /**
0924: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL.
0925: *
0926: * This method is called when a component will be rendered standalone.
0927: *
0928: * @param component
0929: *
0930: */
0931: public final void startComponentRender(Component component) {
0932: renderedComponents = null;
0933: }
0934:
0935: /**
0936: * Get the string representation of this container.
0937: *
0938: * @return String representation of this container
0939: */
0940: public String toString() {
0941: if (versionManager != null) {
0942: return "[Page class = " + getClass().getName() + ", id = "
0943: + getId() + ", version = "
0944: + versionManager.getCurrentVersionNumber()
0945: + ", ajax = "
0946: + versionManager.getAjaxVersionNumber() + "]";
0947: } else {
0948: return "[Page class = " + getClass().getName() + ", id = "
0949: + getId() + ", version = " + 0 + "]";
0950: }
0951: }
0952:
0953: /**
0954: * Throw an exception if not all components rendered.
0955: *
0956: * @param renderedContainer
0957: * The page itself if it was a full page render or the container
0958: * that was rendered standalone
0959: */
0960: private final void checkRendering(
0961: final MarkupContainer renderedContainer) {
0962: // If the application wants component uses checked and
0963: // the response is not a redirect
0964: final IDebugSettings debugSettings = Application.get()
0965: .getDebugSettings();
0966: if (debugSettings.getComponentUseCheck()
0967: && !getResponse().isRedirect()) {
0968: final Count unrenderedComponents = new Count();
0969: final List unrenderedAutoComponents = new ArrayList();
0970: final StringBuffer buffer = new StringBuffer();
0971: renderedContainer.visitChildren(new IVisitor() {
0972: public Object component(final Component component) {
0973: // If component never rendered
0974: if (renderedComponents == null
0975: || !renderedComponents.contains(component)) {
0976: // If auto component ...
0977: if (component.isAuto()) {
0978: // Add to list of unrendered auto components to
0979: // delete below
0980: unrenderedAutoComponents.add(component);
0981: } else if (component.isVisibleInHierarchy()) {
0982: // Increase number of unrendered components
0983: unrenderedComponents.increment();
0984:
0985: // Add to explanatory string to buffer
0986: buffer.append(Integer
0987: .toString(unrenderedComponents
0988: .getCount())
0989: + ". " + component + "\n");
0990: String metadata = (String) component
0991: .getMetaData(Component.CONSTRUCTED_AT_KEY);
0992: if (metadata != null) {
0993: buffer.append(metadata);
0994: }
0995: metadata = (String) component
0996: .getMetaData(Component.ADDED_AT_KEY);
0997: if (metadata != null) {
0998: buffer.append(metadata);
0999: }
1000: } else {
1001: // if the component is not visible in hierarchy we
1002: // should not visit its children since they are also
1003: // not visible
1004: return CONTINUE_TRAVERSAL_BUT_DONT_GO_DEEPER;
1005: }
1006: }
1007: return CONTINUE_TRAVERSAL;
1008: }
1009: });
1010:
1011: // Remove any unrendered auto components since versioning couldn't
1012: // do it. We can't remove the component in the above visitChildren
1013: // callback because we're traversing the list at that time.
1014: for (int i = 0; i < unrenderedAutoComponents.size(); i++) {
1015: ((Component) unrenderedAutoComponents.get(i)).remove();
1016: }
1017:
1018: // Throw exception if any errors were found
1019: if (unrenderedComponents.getCount() > 0) {
1020: // Get rid of set
1021: renderedComponents = null;
1022:
1023: // Throw exception
1024: throw new WicketRuntimeException(
1025: "The component(s) below failed to render. A common problem is that you have added a component in code but forgot to reference it in the markup (thus the component will never be rendered).\n\n"
1026: + buffer.toString());
1027: }
1028: }
1029:
1030: // Get rid of set
1031: renderedComponents = null;
1032: }
1033:
1034: /**
1035: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL OR
1036: * OVERRIDE.
1037: *
1038: */
1039: private final void endVersion() {
1040: // Any changes to the page after this point will be tracked by the
1041: // page's version manager. Since trackChanges is never set to false,
1042: // this effectively means that change tracking begins after the
1043: // first request to a page completes.
1044: setFlag(FLAG_TRACK_CHANGES, true);
1045:
1046: // If a new version was created
1047: if (getFlag(FLAG_NEW_VERSION)) {
1048: // Reset boolean for next request
1049: setFlag(FLAG_NEW_VERSION, false);
1050:
1051: // We're done with this version
1052: if (versionManager != null) {
1053: versionManager.endVersion(getRequest().mergeVersion());
1054: }
1055:
1056: // Evict any page version(s) as need be
1057: getApplication().getSessionSettings()
1058: .getPageMapEvictionStrategy().evict(getPageMap());
1059: }
1060: }
1061:
1062: /**
1063: * Initializes Page by adding it to the Session and initializing it.
1064: */
1065: private final void init() {
1066: final RequestCycle cycle = getRequestCycle();
1067: String pageMapName = null;
1068: if (cycle != null) {
1069: RequestParameters parameters = getRequest()
1070: .getRequestParameters();
1071: pageMapName = parameters.getPageMapName();
1072: }
1073: final IPageMap pageMap = PageMap.forName(pageMapName);
1074: init(pageMap);
1075: }
1076:
1077: /**
1078: * Initializes Page by adding it to the Session and initializing it.
1079: *
1080: * @param pageMap
1081: * The page map to put this page in.
1082: */
1083: private final void init(final IPageMap pageMap) {
1084: // Set the page map
1085: if (pageMap != null) {
1086: setPageMap(pageMap);
1087: } else {
1088: throw new IllegalStateException("PageMap cannot be null");
1089: }
1090:
1091: setNextAvailableId();
1092:
1093: // Set versioning of page based on default
1094: setVersioned(Application.get().getPageSettings()
1095: .getVersionPagesByDefault());
1096:
1097: // All Pages are born dirty so they get clustered right away
1098: dirty();
1099: }
1100:
1101: private void setNextAvailableId() {
1102: if (getApplication().getSessionSettings()
1103: .isPageIdUniquePerSession()) {
1104: setNumericId(getSession().nextPageId());
1105: } else {
1106: // Set the numeric id on this page
1107: setNumericId(getPageMap().nextId());
1108: }
1109: }
1110:
1111: /**
1112: * For the given component, whether we may record changes.
1113: *
1114: * @param component
1115: * The component which is affected
1116: * @param parent
1117: * @return True if the change is okay to report
1118: */
1119: private final boolean mayTrackChangesFor(final Component component,
1120: MarkupContainer parent) {
1121: // Auto components do not participate in versioning since they are
1122: // added during the rendering phase (which is normally illegal).
1123: if (component.isAuto()
1124: || (parent == null && !component.isVersioned())
1125: || (parent != null && !parent.isVersioned())) {
1126: return false;
1127: } else {
1128: // the component is versioned... are we tracking changes at all?
1129: if (getFlag(FLAG_TRACK_CHANGES)) {
1130: // we are tracking changes... do we need to start new version?
1131: if (!getFlag(FLAG_NEW_VERSION)) {
1132: // if we have no version manager
1133: if (versionManager == null) {
1134: // then install a new version manager
1135: versionManager = getSession().getSessionStore()
1136: .newVersionManager(this );
1137: }
1138:
1139: // start a new version
1140: versionManager.beginVersion(getRequest()
1141: .mergeVersion());
1142: setFlag(FLAG_NEW_VERSION, true);
1143: }
1144:
1145: // return true as we are ready for versioning
1146: return true;
1147: }
1148:
1149: // we are not tracking changes or the component not versioned
1150: return false;
1151: }
1152: }
1153:
1154: private void readObject(java.io.ObjectInputStream s)
1155: throws IOException, ClassNotFoundException {
1156: int id = s.readShort();
1157: String name = (String) s.readObject();
1158:
1159: IPageSerializer ps = (IPageSerializer) serializer.get();
1160: if (ps != null) {
1161: pageToResolve = ps.deserializePage(id, name, this , s);
1162: } else {
1163: s.defaultReadObject();
1164: }
1165: }
1166:
1167: // called after readObject
1168: private Object readResolve() throws ObjectStreamException {
1169: if (pageToResolve == null) {
1170: return this ;
1171: } else {
1172: Page page = pageToResolve;
1173: pageToResolve = null;
1174: return page;
1175: }
1176: }
1177:
1178: private void writeObject(java.io.ObjectOutputStream s)
1179: throws IOException {
1180: s.writeShort(numericId);
1181: s.writeObject(pageMapName);
1182:
1183: IPageSerializer ps = (IPageSerializer) serializer.get();
1184:
1185: if (ps != null) {
1186: ps.serializePage(this , s);
1187: } else {
1188: s.defaultWriteObject();
1189: }
1190: }
1191:
1192: /**
1193: * Set-up response with appropriate content type, locale and encoding. The
1194: * locale is set equal to the session's locale. The content type header
1195: * contains information about the markup type (@see #getMarkupType()) and
1196: * the encoding. The response (and request) encoding is determined by an
1197: * application setting (@see
1198: * ApplicationSettings#getResponseRequestEncoding()). In addition, if the
1199: * page's markup contains a xml declaration like <?xml ... ?> an xml
1200: * declaration with proper encoding information is written to the output as
1201: * well, provided it is not disabled by an applicaton setting (@see
1202: * ApplicationSettings#getStripXmlDeclarationFromOutput()).
1203: * <p>
1204: * Note: Prior to Wicket 1.1 the output encoding was determined by the
1205: * page's markup encoding. Because this caused uncertainties about the
1206: * /request/ encoding, it has been changed in favour of the new, much safer,
1207: * approach. Please see the Wiki for more details.
1208: */
1209: protected void configureResponse() {
1210: // Get the response and application
1211: final RequestCycle cycle = getRequestCycle();
1212: final Application application = cycle.getApplication();
1213: final Response response = cycle.getResponse();
1214:
1215: // Determine encoding
1216: final String encoding = application.getRequestCycleSettings()
1217: .getResponseRequestEncoding();
1218:
1219: // Set content type based on markup type for page
1220: response.setContentType("text/" + getMarkupType()
1221: + "; charset=" + encoding);
1222:
1223: // Write out an xml declaration if the markup stream and settings allow
1224: final MarkupStream markupStream = findMarkupStream();
1225: if ((markupStream != null)
1226: && (markupStream.getXmlDeclaration() != null)
1227: && (application.getMarkupSettings()
1228: .getStripXmlDeclarationFromOutput() == false)) {
1229: response.write("<?xml version='1.0' encoding='");
1230: response.write(encoding);
1231: response.write("'?>");
1232: }
1233:
1234: // Set response locale from session locale
1235: response.setLocale(getSession().getLocale());
1236: }
1237:
1238: /**
1239: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL OR
1240: * OVERRIDE.
1241: *
1242: * @see org.apache.wicket.Component#internalOnModelChanged()
1243: */
1244: protected final void internalOnModelChanged() {
1245: visitChildren(new Component.IVisitor() {
1246: public Object component(final Component component) {
1247: // If form component is using form model
1248: if (component.sameInnermostModel(Page.this )) {
1249: component.modelChanged();
1250: }
1251: return IVisitor.CONTINUE_TRAVERSAL;
1252: }
1253: });
1254: }
1255:
1256: /**
1257: * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL OR
1258: * OVERRIDE.
1259: *
1260: * @param map
1261: */
1262: protected final void moveToPageMap(IPageMap map) {
1263: // TODO post 1.2 shouldn't we remove this page from the pagemap/session
1264: // if it would be in there?
1265: // This should be done if the page was not cloned first, but shouldn't
1266: // be done if it was cloned..
1267: setPageMap(map);
1268:
1269: setNextAvailableId();
1270: }
1271:
1272: /**
1273: * @return Factory method that creates a version manager for this Page
1274: */
1275: protected final IPageVersionManager newVersionManager() {
1276: return null;
1277: }
1278:
1279: /**
1280: * @see org.apache.wicket.Component#onDetach()
1281: */
1282: protected void onDetach() {
1283: if (log.isDebugEnabled()) {
1284: log.debug("ending request for page " + this + ", request "
1285: + getRequest());
1286: }
1287:
1288: endVersion();
1289:
1290: dirty();
1291:
1292: super .onDetach();
1293:
1294: }
1295:
1296: /**
1297: * Renders this container to the given response object.
1298: *
1299: * @param markupStream
1300: */
1301: protected void onRender(final MarkupStream markupStream) {
1302: // Set page's associated markup stream
1303: final MarkupStream associatedMarkupStream = getAssociatedMarkupStream(true);
1304: setMarkupStream(associatedMarkupStream);
1305:
1306: // Configure response object with locale and content type
1307: configureResponse();
1308:
1309: // Render markup
1310: renderAll(associatedMarkupStream);
1311: }
1312:
1313: /**
1314: * A component was added.
1315: *
1316: * @param component
1317: * The component that was added
1318: */
1319: final void componentAdded(final Component component) {
1320: dirty();
1321: if (mayTrackChangesFor(component, component.getParent())) {
1322: versionManager.componentAdded(component);
1323: }
1324: }
1325:
1326: /**
1327: * A component's model changed.
1328: *
1329: * @param component
1330: * The component whose model is about to change
1331: */
1332: final void componentModelChanging(final Component component) {
1333: dirty();
1334: if (mayTrackChangesFor(component, null)) {
1335: versionManager.componentModelChanging(component);
1336: }
1337: }
1338:
1339: /**
1340: * A component was removed.
1341: *
1342: * @param component
1343: * The component that was removed
1344: */
1345: final void componentRemoved(final Component component) {
1346: dirty();
1347: if (mayTrackChangesFor(component, component.getParent())) {
1348: versionManager.componentRemoved(component);
1349: }
1350: }
1351:
1352: final void componentStateChanging(final Component component,
1353: Change change) {
1354: dirty();
1355: if (mayTrackChangesFor(component, null)) {
1356: versionManager.componentStateChanging(change);
1357: }
1358: }
1359:
1360: /**
1361: * Sets values for form components based on cookie values in the request.
1362: *
1363: */
1364: final void setFormComponentValuesFromCookies() {
1365: // Visit all Forms contained in the page
1366: visitChildren(Form.class, new Component.IVisitor() {
1367: // For each FormComponent found on the Page (not Form)
1368: public Object component(final Component component) {
1369: ((Form) component).loadPersistentFormComponentValues();
1370: return CONTINUE_TRAVERSAL;
1371: }
1372: });
1373: }
1374:
1375: /**
1376: * @param pageMap
1377: * Sets this page into the page map with the given name. If the
1378: * page map does not yet exist, it is automatically created.
1379: */
1380: final void setPageMap(final IPageMap pageMap) {
1381: // Save transient reference to pagemap
1382: this .pageMap = pageMap;
1383:
1384: // Save name for restoring transient
1385: pageMapName = pageMap.getName();
1386: }
1387:
1388: /**
1389: * Set page stateless
1390: *
1391: * @param stateless
1392: */
1393: void setPageStateless(Boolean stateless) {
1394: this.stateless = stateless;
1395: }
1396: }
|