/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.wicket; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import org.apache.wicket.authorization.UnauthorizedActionException; import org.apache.wicket.core.util.lang.WicketObjects; import org.apache.wicket.markup.MarkupException; import org.apache.wicket.markup.MarkupStream; import org.apache.wicket.markup.MarkupType; import org.apache.wicket.markup.html.WebPage; import org.apache.wicket.markup.resolver.IComponentResolver; import org.apache.wicket.model.IModel; import org.apache.wicket.page.IPageManager; import org.apache.wicket.pageStore.IPageStore; import org.apache.wicket.request.component.IRequestablePage; import org.apache.wicket.request.cycle.RequestCycle; import org.apache.wicket.request.mapper.parameter.PageParameters; import org.apache.wicket.settings.DebugSettings; import org.apache.wicket.util.lang.Classes; import org.apache.wicket.util.lang.Generics; import org.apache.wicket.util.string.StringValue; import org.apache.wicket.util.visit.IVisit; import org.apache.wicket.util.visit.IVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Abstract base class for pages. As a {@link MarkupContainer} subclass, a Page can contain a * component hierarchy and markup in some markup language such as HTML. Users of the framework * should not attempt to subclass Page directly. Instead they should subclass a subclass of Page * that is appropriate to the markup type they are using, such as {@link WebPage} (for HTML markup). * <p> * Page has the following differences to {@link Component}s main concepts: * <ul> * <li><b>Identity </b>- Page numerical identifiers start at 0 for each {@link Session} and * increment for each new page. This numerical identifier is used as the component identifier * accessible via {@link #getId()}.</li> * <li><b>Construction </b>- When a page is constructed, it is automatically registerd with the * application's {@link IPageManager}. <br> * Pages can be constructed with any constructor like any other component, but if you wish to link * to a Page using a URL that is "bookmarkable" (which implies that the URL will not have any * session information encoded in it, and that you can call this page directly without having a * session first directly from your browser), you need to implement your Page with a no-arg * constructor or with a constructor that accepts a {@link PageParameters} argument (which wraps any * query string parameters for a request). In case the page has both constructors, the constructor * with PageParameters will be used.</li> * <li><b>Versioning </b>- Pages support the browser's back button when versioning is enabled via * {@link #setVersioned(boolean)}. By default all pages are versioned if not configured differently * in {@link org.apache.wicket.settings.PageSettings#setVersionPagesByDefault(boolean)}</li> * </ul> * * @see org.apache.wicket.markup.html.WebPage * @see org.apache.wicket.MarkupContainer * @see org.apache.wicket.model.CompoundPropertyModel * @see org.apache.wicket.Component * * @author Jonathan Locke * @author Chris Turner * @author Eelco Hillenius * @author Johan Compagner * */ public abstract class Page extends MarkupContainer implements IRequestablePage, IQueueRegion { /** True if the page hierarchy has been modified in the current request. */ private static final int FLAG_IS_DIRTY = FLAG_RESERVED3; /** Set to prevent marking page as dirty under certain circumstances. */ private static final int FLAG_PREVENT_DIRTY = FLAG_RESERVED4; /** True if the page should try to be stateless */ private static final int FLAG_STATELESS_HINT = FLAG_RESERVED5; /** Flag that indicates if the page was created using one of its bookmarkable constructors */ private static final int FLAG_WAS_CREATED_BOOKMARKABLE = FLAG_RESERVED8; /** Log. */ private static final Logger log = LoggerFactory.getLogger(Page.class); private static final long serialVersionUID = 1L; /** Used to create page-unique numbers */ private int autoIndex; /** Numeric version of this page's id */ private int numericId; /** Set of components that rendered if component use checking is enabled */ private transient Set<Component> renderedComponents; /** * Boolean if the page is stateless, so it doesn't have to be in the page map, will be set in * urlFor */ private transient Boolean stateless = null; /** Page parameters used to construct this page */ private final PageParameters pageParameters; /** * @see IRequestablePage#getRenderCount() */ private int renderCount = 0; /** * Constructor. */ protected Page() { this(null, null); } /** * Constructor. * * @param model * See Component * @see Component#Component(String, IModel) */ protected Page(final IModel<?> model) { this(null, model); } /** * The {@link PageParameters} parameter will be stored in this page and then those parameters * will be used to create stateless links to this bookmarkable page. * * @param parameters * externally passed parameters * @see PageParameters */ protected Page(final PageParameters parameters) { this(parameters, null); } /** * Construct. * * @param parameters * @param model */ private Page(final PageParameters parameters, IModel<?> model) { super(null, model); if (parameters == null) { pageParameters = new PageParameters(); } else { pageParameters = parameters; } } /** * The {@link PageParameters} object that was used to construct this page. This will be used in * creating stateless/bookmarkable links to this page * * @return {@link PageParameters} The construction page parameter */ @Override public PageParameters getPageParameters() { return pageParameters; } /** * Adds a component to the set of rendered components. * * @param component * The component that was rendered */ public final void componentRendered(final Component component) { // Inform the page that this component rendered if (getApplication().getDebugSettings().getComponentUseCheck()) { if (renderedComponents == null) { renderedComponents = new HashSet<Component>(); } if (renderedComponents.add(component) == false) { throw new MarkupException( "The component " + component + " was rendered already. You can render it only once during a render phase. Class relative path: " + component.getClassRelativePath()); } log.debug("Rendered {}", component); } } /** * Detaches any attached models referenced by this page. */ @Override public void detachModels() { super.detachModels(); } @Override public void internalPrepareForRender(boolean setRenderingFlag) { if (!isInitialized()) { // initialize the page if not yet initialized internalInitialize(); } super.internalPrepareForRender(setRenderingFlag); } /** * @see #dirty(boolean) */ public final void dirty() { dirty(false); } /** {@inheritDoc} */ @Override public boolean setFreezePageId(boolean freeze) { boolean frozen = getFlag(FLAG_PREVENT_DIRTY); setFlag(FLAG_PREVENT_DIRTY, freeze); return frozen; } /** * Mark this page as modified in the session. If versioning is supported then a new version of * the page will be stored in {@link IPageStore page store} * * @param isInitialization * a flag whether this is a page instantiation */ public void dirty(final boolean isInitialization) { checkHierarchyChange(this); if (getFlag(FLAG_PREVENT_DIRTY)) { return; } final IPageManager pageManager = getSession().getPageManager(); if (!getFlag(FLAG_IS_DIRTY) && (isVersioned() && pageManager.supportsVersioning() || // we need to get pageId for new page instances even when the page doesn't need // versioning, otherwise pages override each other in the page store and back button // support is broken isInitialization)) { setFlag(FLAG_IS_DIRTY, true); setNextAvailableId(); if (isInitialization == false) { pageManager.touchPage(this); } } } @Override protected void onInitialize() { super.onInitialize(); final IPageManager pageManager = getSession().getPageManager(); pageManager.touchPage(this); } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL. * * This method is called when a component was rendered standalone. If it is a <code> * MarkupContainer</code> then the rendering for that container is checked. * * @param component */ public final void endComponentRender(Component component) { if (component instanceof MarkupContainer) { checkRendering((MarkupContainer)component); } else { renderedComponents = null; } } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL IT. * * Get a page unique number, which will be increased with each call. * * @return A page unique number */ public final int getAutoIndex() { return autoIndex++; } @Override public final String getId() { return Integer.toString(numericId); } /** * * @return page class */ public final Class<? extends Page> getPageClass() { return getClass(); } /** * @return Size of this page in bytes */ @Override public final long getSizeInBytes() { return WicketObjects.sizeof(this); } /** * Returns whether the page should try to be stateless. To be stateless, getStatelessHint() of * every component on page (and it's behavior) must return true and the page must be * bookmarkable. * * @see org.apache.wicket.Component#getStatelessHint() */ @Override public final boolean getStatelessHint() { return getFlag(FLAG_STATELESS_HINT); } /** * @return This page's component hierarchy as a string */ public final String hierarchyAsString() { final StringBuilder buffer = new StringBuilder(); buffer.append("Page ").append(getId()); visitChildren(new IVisitor<Component, Void>() { @Override public void component(final Component component, final IVisit<Void> visit) { int levels = 0; for (Component current = component; current != null; current = current.getParent()) { levels++; } buffer.append(StringValue.repeat(levels, " ")) .append(component.getPageRelativePath()) .append(':') .append(Classes.simpleName(component.getClass())); } }); return buffer.toString(); } /** * Bookmarkable page can be instantiated using a bookmarkable URL. * * @return Returns true if the page is bookmarkable. */ @Override public boolean isBookmarkable() { return getApplication().getPageFactory().isBookmarkable(getClass()); } /** * Override this method and return true if your page is used to display Wicket errors. This can * help the framework prevent infinite failure loops. * * @return True if this page is intended to display an error to the end user. */ public boolean isErrorPage() { return false; } /** * Determine the "statelessness" of the page while not changing the cached value. * * @return boolean value */ private boolean peekPageStateless() { Boolean old = stateless; Boolean res = isPageStateless(); stateless = old; return res; } /** * Gets whether the page is stateless. Components on stateless page must not render any stateful * urls, and components on stateful page must not render any stateless urls. Stateful urls are * urls, which refer to a certain (current) page instance. * * @return Whether this page is stateless */ @Override public final boolean isPageStateless() { if (isBookmarkable() == false) { stateless = Boolean.FALSE; if (getStatelessHint()) { log.warn("Page '" + this + "' is not stateless because it is not bookmarkable, " + "but the stateless hint is set to true!"); } } if (getStatelessHint() == false) { return false; } if (stateless == null) { internalInitialize(); if (isStateless() == false) { stateless = Boolean.FALSE; } } if (stateless == null) { Component statefulComponent = visitChildren(Component.class, new IVisitor<Component, Component>() { @Override public void component(final Component component, final IVisit<Component> visit) { if (!component.isStateless()) { visit.stop(component); } } }); stateless = statefulComponent == null; if (log.isDebugEnabled() && !stateless.booleanValue() && getStatelessHint()) { log.debug("Page '{}' is not stateless because of component with path '{}'.", this, statefulComponent.getPageRelativePath()); } } return stateless; } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL. * * Set the id for this Page. This method is called by PageMap when a Page is added because the * id, which is assigned by PageMap, is not known until this time. * * @param id * The id */ public final void setNumericId(final int id) { numericId = id; } /** * Sets whether the page should try to be stateless. To be stateless, getStatelessHint() of * every component on page (and it's behavior) must return true and the page must be * bookmarkable. * * @param value * whether the page should try to be stateless */ public final Page setStatelessHint(boolean value) { if (value && !isBookmarkable()) { throw new WicketRuntimeException( "Can't set stateless hint to true on a page when the page is not bookmarkable, page: " + this); } setFlag(FLAG_STATELESS_HINT, value); return this; } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL. * * This method is called when a component will be rendered standalone. * * @param component * */ public final void startComponentRender(Component component) { renderedComponents = null; } /** * Get the string representation of this container. * * @return String representation of this container */ @Override public String toString() { return "[Page class = " + getClass().getName() + ", id = " + getId() + ", render count = " + getRenderCount() + "]"; } /** * Throw an exception if not all components rendered. * * @param renderedContainer * The page itself if it was a full page render or the container that was rendered * standalone */ private void checkRendering(final MarkupContainer renderedContainer) { // If the application wants component uses checked and // the response is not a redirect final DebugSettings debugSettings = getApplication().getDebugSettings(); if (debugSettings.getComponentUseCheck()) { final List<Component> unrenderedComponents = new ArrayList<Component>(); final StringBuilder buffer = new StringBuilder(); renderedContainer.visitChildren(new IVisitor<Component, Void>() { @Override public void component(final Component component, final IVisit<Void> visit) { // If component never rendered if (renderedComponents == null || !renderedComponents.contains(component)) { // If not an auto component ... if (!component.isAuto() && component.isVisibleInHierarchy()) { // Increase number of unrendered components unrenderedComponents.add(component); // Add to explanatory string to buffer buffer.append(Integer.toString(unrenderedComponents.size())) .append(". ") .append(component.toString(true)) .append('\n'); String metadata = component.getMetaData(Component.CONSTRUCTED_AT_KEY); if (metadata != null) { buffer.append(metadata).append('\n'); } metadata = component.getMetaData(Component.ADDED_AT_KEY); if (metadata != null) { buffer.append(metadata).append('\n'); } } else { // if the component is not visible in hierarchy we // should not visit its children since they are also // not visible visit.dontGoDeeper(); } } } }); // Throw exception if any errors were found if (unrenderedComponents.size() > 0) { renderedComponents = null; List<Component> transparentContainerChildren = Generics.newArrayList(); Iterator<Component> iterator = unrenderedComponents.iterator(); outerWhile : while (iterator.hasNext()) { Component component = iterator.next(); // If any of the transparentContainerChildren is a parent to component, then // ignore it. for (Component transparentContainerChild : transparentContainerChildren) { MarkupContainer parent = component.getParent(); while (parent != null) { if (parent == transparentContainerChild) { iterator.remove(); continue outerWhile; } parent = parent.getParent(); } } if (hasInvisibleTransparentChild(component.getParent(), component)) { // If we found a transparent container that isn't visible then ignore this // component and only do a debug statement here. if (log.isDebugEnabled()) { log.debug( "Component {} wasn't rendered but might have a transparent parent.", component); } transparentContainerChildren.add(component); iterator.remove(); continue outerWhile; } } // if still > 0 if (unrenderedComponents.size() > 0) { // Throw exception throw new WicketRuntimeException( "The component(s) below failed to render. Possible reasons could be that:\n\t1) you have added a component in code but forgot to reference it in the markup (thus the component will never be rendered),\n\t2) if your components were added in a parent container then make sure the markup for the child container includes them in <wicket:extend>.\n\n" + buffer.toString()); } } } // Get rid of set renderedComponents = null; } private boolean hasInvisibleTransparentChild(final MarkupContainer root, final Component self) { for (Component sibling : root) { if ((sibling != self) && (sibling instanceof IComponentResolver) && (sibling instanceof MarkupContainer)) { if (!sibling.isVisible()) { return true; } else { boolean rtn = hasInvisibleTransparentChild((MarkupContainer)sibling, self); if (rtn == true) { return true; } } } } return false; } /** * Initializes Page by adding it to the Session and initializing it. */ @Override void init() { if (isBookmarkable() == false) { setStatelessHint(false); } // Set versioning of page based on default setVersioned(getApplication().getPageSettings().getVersionPagesByDefault()); // All Pages are born dirty so they get clustered right away dirty(true); // this is a bit of a dirty hack, but calling dirty(true) results in isStateless called // which is bound to set the stateless cache to true as there are no components yet stateless = null; } private void setNextAvailableId() { setNumericId(getSession().nextPageId()); } /** * This method will be called for all components that are changed on the page So also auto * components or components that are not versioned. * * If the parent is given that it was a remove or add from that parent of the given component. * else it was just a internal property change of that component. * * @param component * @param parent */ protected void componentChanged(Component component, MarkupContainer parent) { if (!component.isAuto()) { dirty(); } } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL OR OVERRIDE. * * @see org.apache.wicket.Component#internalOnModelChanged() */ @Override protected final void internalOnModelChanged() { visitChildren(new IVisitor<Component, Void>() { @Override public void component(final Component component, final IVisit<Void> visit) { // If form component is using form model if (component.sameInnermostModel(Page.this)) { component.modelChanged(); } } }); } @Override void internalOnAfterConfigure() { super.internalOnAfterConfigure(); // first try to check if the page can be rendered: if (!isRenderAllowed()) { if (log.isDebugEnabled()) { log.debug("Page not allowed to render: " + this); } throw new UnauthorizedActionException(this, Component.RENDER); } } @Override protected void onBeforeRender() { // Make sure it is really empty renderedComponents = null; // rendering might remove or add stateful components, so clear flag to force reevaluation stateless = null; super.onBeforeRender(); // If any of the components on page is not stateless, we need to bind the session // before we start rendering components, as then jsessionid won't be appended // for links rendered before first stateful component if (getSession().isTemporary() && !peekPageStateless()) { getSession().bind(); } } @Override protected void onAfterRender() { super.onAfterRender(); // Check rendering if it happened fully checkRendering(this); // clean up debug meta data if component check is on if (getApplication().getDebugSettings().getComponentUseCheck()) { visitChildren(new IVisitor<Component, Void>() { @Override public void component(final Component component, final IVisit<Void> visit) { component.setMetaData(Component.CONSTRUCTED_AT_KEY, null); component.setMetaData(Component.ADDED_AT_KEY, null); } }); } if (!isPageStateless()) { // trigger creation of the actual session in case it was deferred getSession().getSessionStore().getSessionId(RequestCycle.get().getRequest(), true); // Add/touch the response page in the session. getSession().getPageManager().touchPage(this); } if (getApplication().getDebugSettings().isOutputMarkupContainerClassName()) { String className = Classes.name(getClass()); getResponse().write("<!-- Page Class "); getResponse().write(className); getResponse().write(" END -->\n"); } } @Override protected void onDetach() { if (log.isDebugEnabled()) { log.debug("ending request for page " + this + ", request " + getRequest()); } setFlag(FLAG_IS_DIRTY, false); super.onDetach(); } @Override protected void onRender() { // Loop through the markup in this container MarkupStream markupStream = new MarkupStream(getMarkup()); renderAll(markupStream, null); } /** * A component was added. * * @param component * The component that was added */ final void componentAdded(final Component component) { if (!component.isAuto()) { dirty(); } } /** * A component's model changed. * * @param component * The component whose model is about to change */ final void componentModelChanging(final Component component) { dirty(); } /** * A component was removed. * * @param component * The component that was removed */ final void componentRemoved(final Component component) { if (!component.isAuto()) { dirty(); } } /** * * @param component */ final void componentStateChanging(final Component component) { if (!component.isAuto()) { dirty(); } } /** * Set page stateless * * @param stateless */ void setPageStateless(Boolean stateless) { this.stateless = stateless; } @Override public MarkupType getMarkupType() { throw new UnsupportedOperationException( "Page does not support markup. This error can happen if you have extended Page directly, instead extend WebPage"); } /** * Gets page instance's unique identifier * * @return instance unique identifier */ public PageReference getPageReference() { setStatelessHint(false); // make sure the page will be available on following request getSession().getPageManager().touchPage(this); return new PageReference(numericId); } @Override public int getPageId() { return numericId; } @Override public int getRenderCount() { return renderCount; } /** * THIS METHOD IS NOT PART OF WICKET API. DO NOT USE! * * Sets the flag that determines whether or not this page was created using one of its * bookmarkable constructors * * @param wasCreatedBookmarkable */ public final void setWasCreatedBookmarkable(boolean wasCreatedBookmarkable) { setFlag(FLAG_WAS_CREATED_BOOKMARKABLE, wasCreatedBookmarkable); } /** * Checks if this page was created using one of its bookmarkable constructors * * @see org.apache.wicket.request.component.IRequestablePage#wasCreatedBookmarkable() */ @Override public final boolean wasCreatedBookmarkable() { return getFlag(FLAG_WAS_CREATED_BOOKMARKABLE); } @Override public void renderPage() { // page id is frozen during the render final boolean frozen = setFreezePageId(true); try { ++renderCount; render(); // stateless = null; } finally { setFreezePageId(frozen); } } /** * THIS METHOD IS NOT PART OF THE WICKET PUBLIC API. DO NOT CALL. * * @param component * @return if this component was render in this page */ public final boolean wasRendered(Component component) { return renderedComponents != null && renderedComponents.contains(component); } }