org.apache.wicket.Page.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.Page.java

Source

/*
 * 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.feedback.FeedbackDelay;
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
    protected void onConfigure() {
        if (!isInitialized()) {
            // initialize the page if not yet initialized
            internalInitialize();
        }

        super.onConfigure();
    }

    /**
     * @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 called when a component was rendered as a part. If it is a <code>
     * MarkupContainer</code> then the rendering for that container is checked.
     * 
     * @param component
     * 
     * @see Component#renderPart()
     */
    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 called when a component will be rendered as a part.
     * 
     * @param component
     * 
     * @see Component#renderPart()
     */
    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;

            // delay rendering of feedbacks after all other components
            try (FeedbackDelay delay = new FeedbackDelay(getRequestCycle())) {
                beforeRender();

                delay.beforeRender();
            }

            markRendering(true);

            render();
        } 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);
    }
}