org.apache.wicket.core.request.mapper.AbstractBookmarkableMapper.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.wicket.core.request.mapper.AbstractBookmarkableMapper.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.core.request.mapper;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;

import org.apache.wicket.IRequestListener;
import org.apache.wicket.Session;
import org.apache.wicket.core.request.handler.BookmarkableListenerRequestHandler;
import org.apache.wicket.core.request.handler.BookmarkablePageRequestHandler;
import org.apache.wicket.core.request.handler.IPageRequestHandler;
import org.apache.wicket.core.request.handler.ListenerRequestHandler;
import org.apache.wicket.core.request.handler.PageAndComponentProvider;
import org.apache.wicket.core.request.handler.PageProvider;
import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
import org.apache.wicket.protocol.http.PageExpiredException;
import org.apache.wicket.protocol.http.WebApplication;
import org.apache.wicket.request.IRequestHandler;
import org.apache.wicket.request.IRequestHandlerDelegate;
import org.apache.wicket.request.Request;
import org.apache.wicket.request.Url;
import org.apache.wicket.request.component.IRequestablePage;
import org.apache.wicket.request.cycle.RequestCycle;
import org.apache.wicket.request.http.WebRequest;
import org.apache.wicket.request.mapper.info.ComponentInfo;
import org.apache.wicket.request.mapper.info.PageComponentInfo;
import org.apache.wicket.request.mapper.info.PageInfo;
import org.apache.wicket.request.mapper.parameter.INamedParameters;
import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
import org.apache.wicket.request.mapper.parameter.PageParameters;
import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
import org.apache.wicket.util.lang.Args;

/**
 * Abstract encoder for Bookmarkable, Hybrid and BookmarkableListener URLs.
 * 
 * @author Matej Knopp
 */
public abstract class AbstractBookmarkableMapper extends AbstractComponentMapper {

    /**
     * A flag that is used when comparing the mounted paths' segments against
     * the request's url ones.
     *
     * @see #setCaseSensitiveMatch(boolean)
     */
    private boolean isCaseSensitive = true;

    /**
     * Represents information stored in URL.
     * 
     * @author Matej Knopp
     */
    protected static final class UrlInfo {
        private final PageComponentInfo pageComponentInfo;
        private final PageParameters pageParameters;
        private final Class<? extends IRequestablePage> pageClass;

        /**
         * Construct.
         * 
         * @param pageComponentInfo
         *            optional parameter providing the page instance and component information
         * @param pageClass
         *            mandatory parameter
         * @param pageParameters
         *            optional parameter providing pageParameters
         */
        public UrlInfo(PageComponentInfo pageComponentInfo, Class<? extends IRequestablePage> pageClass,
                PageParameters pageParameters) {
            Args.notNull(pageClass, "pageClass");

            this.pageComponentInfo = pageComponentInfo;
            this.pageParameters = cleanPageParameters(pageParameters);

            this.pageClass = pageClass;
        }

        /**
         * Cleans the original parameters from entries used by Wicket internals.
         * 
         * @param originalParameters
         *            the current request's non-modified parameters
         * @return all parameters but Wicket internal ones
         */
        private PageParameters cleanPageParameters(final PageParameters originalParameters) {
            PageParameters cleanParameters = null;
            if (originalParameters != null) {
                cleanParameters = new PageParameters(originalParameters);

                // WICKET-4038: Ajax related parameters are set by wicket-ajax.js when needed.
                // They shouldn't be propagated to the next requests
                cleanParameters.remove(WebRequest.PARAM_AJAX);
                cleanParameters.remove(WebRequest.PARAM_AJAX_BASE_URL);
                cleanParameters.remove(WebRequest.PARAM_AJAX_REQUEST_ANTI_CACHE);

                if (cleanParameters.isEmpty()) {
                    cleanParameters = null;
                }
            }
            return cleanParameters;
        }

        /**
         * @return PageComponentInfo instance or <code>null</code>
         */
        public PageComponentInfo getPageComponentInfo() {
            return pageComponentInfo;
        }

        /**
         * @return page class
         */
        public Class<? extends IRequestablePage> getPageClass() {
            return pageClass;
        }

        /**
         * @return PageParameters instance (never <code>null</code>)
         */
        public PageParameters getPageParameters() {
            return pageParameters;
        }
    }

    protected final List<MountPathSegment> pathSegments;

    protected final String[] mountSegments;

    protected final IPageParametersEncoder pageParametersEncoder;

    /**
     * Construct.
     */
    public AbstractBookmarkableMapper() {
        this("notUsed", new PageParametersEncoder());
    }

    public AbstractBookmarkableMapper(String mountPath, IPageParametersEncoder pageParametersEncoder) {
        Args.notEmpty(mountPath, "mountPath");

        this.pageParametersEncoder = Args.notNull(pageParametersEncoder, "pageParametersEncoder");
        mountSegments = getMountSegments(mountPath);
        pathSegments = getPathSegments(mountSegments);
    }

    /**
     * Parse the given request to an {@link UrlInfo} instance.
     * 
     * @param request
     * @return UrlInfo instance or <code>null</code> if this encoder can not handle the request
     */
    protected abstract UrlInfo parseRequest(Request request);

    /**
     * Builds URL for the given {@link UrlInfo} instance. The URL this method produces must be
     * parseable by the {@link #parseRequest(Request)} method.
     * 
     * @param info
     * @return Url result URL
     */
    protected abstract Url buildUrl(UrlInfo info);

    /**
     * Indicates whether hybrid {@link RenderPageRequestHandler} URL for page will be generated only
     * if page has been created with bookmarkable URL.
     * <p>
     * For generic bookmarkable encoders this method should return <code>true</code>. For explicit
     * (mounted) encoders this method should return <code>false</code>
     * 
     * @return <code>true</code> if hybrid URL requires page created bookmarkable,
     *         <code>false</code> otherwise.
     */
    protected abstract boolean pageMustHaveBeenCreatedBookmarkable();

    @Override
    public int getCompatibilityScore(Request request) {
        if (urlStartsWith(request.getUrl(), mountSegments)) {
            /* see WICKET-5056 - alter score with pathSegment type */
            int countOptional = 0;
            int fixedSegments = 0;
            for (MountPathSegment pathSegment : pathSegments) {
                fixedSegments += pathSegment.getFixedPartSize();
                countOptional += pathSegment.getOptionalParameters();
            }
            return mountSegments.length - countOptional + fixedSegments;
        } else {
            return 0;
        }
    }

    /**
     * Creates a {@code IRequestHandler} that processes a bookmarkable request.
     * 
     * @param pageClass
     * @param pageParameters
     * @return a {@code IRequestHandler} capable of processing the bookmarkable request.
     */
    protected IRequestHandler processBookmarkable(Class<? extends IRequestablePage> pageClass,
            PageParameters pageParameters) {
        PageProvider provider = new PageProvider(pageClass, pageParameters);
        provider.setPageSource(getContext());
        return new RenderPageRequestHandler(provider);
    }

    /**
     * Creates a {@code IRequestHandler} that processes a hybrid request. When the page identified
     * by {@code pageInfo} was not available, the request should be treated as a bookmarkable
     * request.
     * 
     * @param pageInfo
     * @param pageClass
     * @param pageParameters
     * @param renderCount
     * @return a {@code IRequestHandler} capable of processing the hybrid request.
     */
    protected IRequestHandler processHybrid(PageInfo pageInfo, Class<? extends IRequestablePage> pageClass,
            PageParameters pageParameters, Integer renderCount) {
        PageProvider provider = new PageProvider(pageInfo.getPageId(), pageClass, pageParameters, renderCount);
        provider.setPageSource(getContext());

        checkExpiration(provider, pageInfo);

        /**
         * https://issues.apache.org/jira/browse/WICKET-5734
         * */
        PageParameters constructionPageParameters = provider.hasPageInstance()
                ? provider.getPageInstance().getPageParameters()
                : new PageParameters();

        if (PageParameters.equals(constructionPageParameters, pageParameters) == false) {
            // create a fresh page instance because the request page parameters are different than the ones
            // when the resolved page by id has been created
            return new RenderPageRequestHandler(new PageProvider(pageClass, pageParameters));
        }
        return new RenderPageRequestHandler(provider);
    }

    boolean getRecreateMountedPagesAfterExpiry() {
        return WebApplication.get().getPageSettings().getRecreateBookmarkablePagesAfterExpiry();
    }

    /**
     * Creates a {@code IRequestHandler} that notifies an {@link IRequestListener}.
     * 
     * @param pageComponentInfo
     * @param pageClass
     * @param pageParameters
     * @return a {@code IRequestHandler} that notifies an {@link IRequestListener}.
     */
    protected IRequestHandler processListener(PageComponentInfo pageComponentInfo,
            Class<? extends IRequestablePage> pageClass, PageParameters pageParameters) {
        PageInfo pageInfo = pageComponentInfo.getPageInfo();
        ComponentInfo componentInfo = pageComponentInfo.getComponentInfo();
        Integer renderCount = null;

        if (componentInfo != null) {
            renderCount = componentInfo.getRenderCount();
        }

        PageAndComponentProvider provider = new PageAndComponentProvider(pageInfo.getPageId(), pageClass,
                pageParameters, renderCount, componentInfo.getComponentPath());

        provider.setPageSource(getContext());

        checkExpiration(provider, pageInfo);

        return new ListenerRequestHandler(provider, componentInfo.getBehaviorId());
    }

    private void checkExpiration(PageProvider provider, PageInfo pageInfo) {
        if (provider.wasExpired() && !getRecreateMountedPagesAfterExpiry()) {
            throw new PageExpiredException(
                    String.format("Bookmarkable page with id '%d' has expired.", pageInfo.getPageId()));
        }
    }

    @Override
    public IRequestHandler mapRequest(Request request) {
        UrlInfo urlInfo = parseRequest(request);

        if (urlInfo != null) {
            PageComponentInfo info = urlInfo.getPageComponentInfo();
            Class<? extends IRequestablePage> pageClass = urlInfo.getPageClass();
            PageParameters pageParameters = urlInfo.getPageParameters();

            if (info == null) {
                // if there are is no page instance information
                // then this is a simple bookmarkable URL
                return processBookmarkable(pageClass, pageParameters);
            } else if (info.getPageInfo().getPageId() != null && info.getComponentInfo() == null) {
                // if there is page instance information in the URL but no component and listener
                // interface then this is a hybrid URL - we need to try to reuse existing page
                // instance
                return processHybrid(info.getPageInfo(), pageClass, pageParameters, null);
            } else if (info.getComponentInfo() != null) {
                // with both page instance and component this is a request listener URL
                return processListener(info, pageClass, pageParameters);
            } else if (info.getPageInfo().getPageId() == null) {
                return processBookmarkable(pageClass, pageParameters);
            }

        }
        return null;
    }

    protected boolean checkPageInstance(IRequestablePage page) {
        return page != null && checkPageClass(page.getClass());
    }

    protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass) {
        return true;
    }

    @Override
    public Url mapHandler(IRequestHandler requestHandler) {
        // TODO see if we can refactor this to remove dependency on instanceof checks below and
        // eliminate the need for IRequestHandlerDelegate
        while (requestHandler instanceof IRequestHandlerDelegate) {
            requestHandler = ((IRequestHandlerDelegate) requestHandler).getDelegateHandler();
        }

        if (requestHandler instanceof BookmarkablePageRequestHandler) {
            // simple bookmarkable URL with no page instance information
            BookmarkablePageRequestHandler handler = (BookmarkablePageRequestHandler) requestHandler;

            if (!checkPageClass(handler.getPageClass())) {
                return null;
            }

            PageInfo info = new PageInfo();
            UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(info, null), handler.getPageClass(),
                    handler.getPageParameters());

            return buildUrl(urlInfo);
        } else if (requestHandler instanceof RenderPageRequestHandler) {
            // possibly hybrid URL - bookmarkable URL with page instance information
            // but only allowed if the page was created by bookmarkable URL

            RenderPageRequestHandler handler = (RenderPageRequestHandler) requestHandler;

            if (!checkPageClass(handler.getPageClass())) {
                return null;
            }

            if (handler.getPageProvider().isNewPageInstance()) {
                // no existing page instance available, don't bother creating new page instance
                PageInfo info = new PageInfo();
                UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(info, null), handler.getPageClass(),
                        handler.getPageParameters());

                return buildUrl(urlInfo);
            }

            IRequestablePage page = handler.getPage();

            if (checkPageInstance(page)
                    && (!pageMustHaveBeenCreatedBookmarkable() || page.wasCreatedBookmarkable())) {
                PageInfo info = getPageInfo(handler);
                PageComponentInfo pageComponentInfo = new PageComponentInfo(info, null);

                UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(), handler.getPageParameters());
                return buildUrl(urlInfo);
            } else {
                return null;
            }

        } else if (requestHandler instanceof BookmarkableListenerRequestHandler) {
            // request listener URL with page class information
            BookmarkableListenerRequestHandler handler = (BookmarkableListenerRequestHandler) requestHandler;
            Class<? extends IRequestablePage> pageClass = handler.getPageClass();

            if (!checkPageClass(pageClass)) {
                return null;
            }

            Integer renderCount = null;
            if (handler.includeRenderCount()) {
                renderCount = handler.getRenderCount();
            }

            PageInfo pageInfo = getPageInfo(handler);
            ComponentInfo componentInfo = new ComponentInfo(renderCount, handler.getComponentPath(),
                    handler.getBehaviorIndex());

            PageParameters parameters = getRecreateMountedPagesAfterExpiry()
                    ? new PageParameters(handler.getPage().getPageParameters())
                            .mergeWith(handler.getPageParameters())
                    : handler.getPageParameters();
            UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(pageInfo, componentInfo), pageClass, parameters);
            return buildUrl(urlInfo);
        }

        return null;
    }

    protected final PageInfo getPageInfo(IPageRequestHandler handler) {
        Args.notNull(handler, "handler");

        Integer pageId = null;
        if (handler.isPageInstanceCreated()) {
            IRequestablePage page = handler.getPage();

            if (page.isPageStateless() == false) {
                pageId = page.getPageId();
            }
        }

        return new PageInfo(pageId);
    }

    /**
     * @return a new instance of {@link PageParameters} that will be passed to the page/resource
     */
    protected PageParameters newPageParameters() {
        final PageParameters parameters = new PageParameters();
        parameters.setLocale(resolveLocale());
        return parameters;
    }

    /**
     * Override {@link #resolveLocale()} to return the result of this method if you want to use
     * the user's session or request locale for parsing numbers from the page parameters
     *
     * @return the Session or Request's locale to use for parsing any numbers in the request parameters
     */
    protected Locale resolveUserLocale() {
        Locale locale = super.resolveLocale();
        if (Session.exists()) {
            locale = Session.get().getLocale();
        } else {
            RequestCycle requestCycle = RequestCycle.get();
            if (requestCycle != null) {
                Request request = requestCycle.getRequest();
                if (request != null) {
                    locale = request.getLocale();
                }
            }
        }

        return locale;
    }

    protected static class MountPathSegment {
        private int segmentIndex;
        private String fixedPart;
        private int minParameters;
        private int optionalParameters;

        public MountPathSegment(int segmentIndex) {
            this.segmentIndex = segmentIndex;
        }

        public void setFixedPart(String fixedPart) {
            this.fixedPart = fixedPart;
        }

        public void addRequiredParameter() {
            minParameters++;
        }

        public void addOptionalParameter() {
            optionalParameters++;
        }

        public int getSegmentIndex() {
            return segmentIndex;
        }

        public String getFixedPart() {
            return fixedPart;
        }

        public int getMinParameters() {
            return minParameters;
        }

        public int getOptionalParameters() {
            return optionalParameters;
        }

        public int getMaxParameters() {
            return getOptionalParameters() + getMinParameters();
        }

        public int getFixedPartSize() {
            return getFixedPart() == null ? 0 : 1;
        }

        @Override
        public String toString() {
            return "(" + getSegmentIndex() + ") " + getMinParameters() + '-' + getMaxParameters() + ' '
                    + (getFixedPart() == null ? "(end)" : getFixedPart());
        }
    }

    protected List<MountPathSegment> getPathSegments(String[] segments) {
        List<MountPathSegment> ret = new ArrayList<MountPathSegment>();
        int segmentIndex = 0;
        MountPathSegment curPathSegment = new MountPathSegment(segmentIndex);
        ret.add(curPathSegment);
        for (String curSegment : segments) {
            if (isFixedSegment(curSegment)) {
                curPathSegment.setFixedPart(curSegment);
                curPathSegment = new MountPathSegment(segmentIndex + 1);
                ret.add(curPathSegment);
            } else if (getPlaceholder(curSegment) != null) {
                curPathSegment.addRequiredParameter();
            } else {
                curPathSegment.addOptionalParameter();
            }
            segmentIndex++;
        }
        return ret;
    }

    protected boolean isFixedSegment(String segment) {
        return getOptionalPlaceholder(segment) == null && getPlaceholder(segment) == null;
    }

    /**
     * Extracts the PageParameters from URL if there are any
     */
    protected PageParameters extractPageParameters(Request request, Url url) {
        int[] matchedParameters = getMatchedSegmentSizes(url);

        int total = 0;
        for (int curMatchSize : matchedParameters) {
            total += curMatchSize;
        }
        PageParameters pageParameters = extractPageParameters(request, total, pageParametersEncoder);
        if (pageParameters != null) {
            pageParameters.setLocale(resolveLocale());
        }

        int segmentIndex = 0;
        for (int pathSegmentIndex = 0; pathSegmentIndex < pathSegments.size(); pathSegmentIndex++) {
            MountPathSegment pathSegment = pathSegments.get(pathSegmentIndex);

            int totalAdded = 0;
            int requiredAdded = 0;
            for (int segmentParameterIndex = 0; segmentParameterIndex < pathSegment.getMaxParameters()
                    && totalAdded < matchedParameters[pathSegmentIndex]; segmentParameterIndex++) {
                if (pageParameters == null) {
                    pageParameters = newPageParameters();
                }

                String curSegment = mountSegments[pathSegment.getSegmentIndex() + segmentParameterIndex];

                String placeholder = getPlaceholder(curSegment);
                String optionalPlaceholder = getOptionalPlaceholder(curSegment);
                // extract the parameter from URL
                if (placeholder != null) {
                    pageParameters.add(placeholder, url.getSegments().get(segmentIndex),
                            INamedParameters.Type.PATH);
                    segmentIndex++;
                    totalAdded++;
                    requiredAdded++;
                } else if (optionalPlaceholder != null && matchedParameters[pathSegmentIndex]
                        - segmentParameterIndex > pathSegment.getMinParameters() + pathSegment.getFixedPartSize()
                                - requiredAdded) {
                    pageParameters.add(optionalPlaceholder, url.getSegments().get(segmentIndex),
                            INamedParameters.Type.PATH);
                    segmentIndex++;
                    totalAdded++;
                }
            }

            segmentIndex += pathSegment.getFixedPartSize();
        }
        return pageParameters;
    }

    protected int[] getMatchedSegmentSizes(Url url) {
        int[] ret = new int[pathSegments.size()];
        int segmentIndex = 0;
        int pathSegmentIndex = 0;
        for (MountPathSegment curPathSegment : pathSegments.subList(0, pathSegments.size() - 1)) {
            boolean foundFixedPart = false;
            segmentIndex += curPathSegment.getMinParameters();
            int max = Math.min(curPathSegment.getOptionalParameters() + 1, url.getSegments().size() - segmentIndex);

            for (int count = max - 1; count >= 0; count--) {
                if (segmentsMatch(url.getSegments().get(segmentIndex + count), curPathSegment.getFixedPart())) {
                    foundFixedPart = true;
                    segmentIndex += count + 1;
                    ret[pathSegmentIndex] = count + curPathSegment.getMinParameters() + 1;
                    break;
                }
            }
            if (!foundFixedPart)
                return null;
            pathSegmentIndex++;
        }
        MountPathSegment lastSegment = pathSegments.get(pathSegments.size() - 1);
        segmentIndex += lastSegment.getMinParameters();
        if (segmentIndex > url.getSegments().size())
            return null;
        ret[pathSegmentIndex] = Math.min(lastSegment.getMaxParameters(),
                url.getSegments().size() - segmentIndex + lastSegment.getMinParameters());
        return ret;
    }

    /**
     * Decides whether a segment from the mounted path matches with a segment
     * from the requested url.
     *
     * A custom implementation of this class may use more complex logic to handle
     * spelling errors
     *
     * @param mountedSegment
     *          the segment from the mounted path
     * @param urlSegment
     *          the segment from the request url
     * @return {@code true} if the segments match
     */
    protected boolean segmentsMatch(String mountedSegment, String urlSegment) {
        final boolean result;
        if (isCaseSensitiveMatch()) {
            result = mountedSegment.equals(urlSegment);
        } else {
            result = mountedSegment.equalsIgnoreCase(urlSegment);
        }
        return result;
    }

    /**
     * @return whether the matching of mounted segments against request's url ones should be
     *      case sensitive or not
     */
    protected boolean isCaseSensitiveMatch() {
        return isCaseSensitive;
    }

    /**
     * Sets whether the matching of mounted segments against request's url ones should be
     * case sensitive or not.
     *
     * @param isCaseSensitive
     *          a flag indicating whether the matching of mounted segments against request's
     *          url ones should be case sensitive or not
     * @return this instance, for chaining
     */
    public AbstractBookmarkableMapper setCaseSensitiveMatch(boolean isCaseSensitive) {
        this.isCaseSensitive = isCaseSensitive;
        return this;
    }

    /**
     * Replaces mandatory and optional parameters with their values.
     *
     * If a mandatory parameter is not provided then the method returns {@code false}
     * indicating that there is a problem.
     * Optional parameters with missing values are just dropped.
     *
     * @param parameters
     *          The parameters with the values
     * @param url
     *          The url with the placeholders
     * @return
     *          {@code true} if all mandatory parameters are properly substituted,
     *          {@code false} - otherwise
     */
    protected boolean setPlaceholders(PageParameters parameters, Url url) {
        boolean mandatoryParametersSet = true;

        int dropped = 0;
        for (int i = 0; i < mountSegments.length; ++i) {
            String placeholder = getPlaceholder(mountSegments[i]);
            String optionalPlaceholder = getOptionalPlaceholder(mountSegments[i]);
            if (placeholder != null) {
                if (parameters.getNamedKeys().contains(placeholder)) {
                    url.getSegments().set(i - dropped, parameters.get(placeholder).toString());
                    parameters.remove(placeholder);
                } else {
                    mandatoryParametersSet = false;
                    break;
                }
            } else if (optionalPlaceholder != null) {
                if (parameters.getNamedKeys().contains(optionalPlaceholder)) {
                    url.getSegments().set(i - dropped, parameters.get(optionalPlaceholder).toString(""));
                    parameters.remove(optionalPlaceholder);
                } else {
                    url.getSegments().remove(i - dropped);
                    dropped++;
                }
            }
        }

        return mandatoryParametersSet;
    }

    protected boolean urlStartsWithMountedSegments(Url url) {
        if (url == null) {
            return false;
        } else {
            return getMatchedSegmentSizes(url) != null;
        }
    }
}