001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.wicket.core.request.mapper;
018
019import java.util.ArrayList;
020import java.util.List;
021import java.util.Locale;
022
023import org.apache.wicket.IRequestListener;
024import org.apache.wicket.Session;
025import org.apache.wicket.core.request.handler.BookmarkableListenerRequestHandler;
026import org.apache.wicket.core.request.handler.BookmarkablePageRequestHandler;
027import org.apache.wicket.core.request.handler.IPageRequestHandler;
028import org.apache.wicket.core.request.handler.ListenerRequestHandler;
029import org.apache.wicket.core.request.handler.PageAndComponentProvider;
030import org.apache.wicket.core.request.handler.PageProvider;
031import org.apache.wicket.core.request.handler.RenderPageRequestHandler;
032import org.apache.wicket.protocol.http.PageExpiredException;
033import org.apache.wicket.protocol.http.WebApplication;
034import org.apache.wicket.request.IRequestHandler;
035import org.apache.wicket.request.IRequestHandlerDelegate;
036import org.apache.wicket.request.Request;
037import org.apache.wicket.request.Url;
038import org.apache.wicket.request.component.IRequestablePage;
039import org.apache.wicket.request.cycle.RequestCycle;
040import org.apache.wicket.request.http.WebRequest;
041import org.apache.wicket.request.mapper.info.ComponentInfo;
042import org.apache.wicket.request.mapper.info.PageComponentInfo;
043import org.apache.wicket.request.mapper.info.PageInfo;
044import org.apache.wicket.request.mapper.parameter.INamedParameters;
045import org.apache.wicket.request.mapper.parameter.IPageParametersEncoder;
046import org.apache.wicket.request.mapper.parameter.PageParameters;
047import org.apache.wicket.request.mapper.parameter.PageParametersEncoder;
048import org.apache.wicket.util.lang.Args;
049
050/**
051 * Abstract encoder for Bookmarkable, Hybrid and BookmarkableListener URLs.
052 * 
053 * @author Matej Knopp
054 */
055public abstract class AbstractBookmarkableMapper extends AbstractComponentMapper
056{
057
058        /**
059         * A flag that is used when comparing the mounted paths' segments against
060         * the request's url ones.
061         *
062         * @see #setCaseSensitiveMatch(boolean)
063         */
064        private boolean isCaseSensitive = true;
065
066        /**
067         * Represents information stored in URL.
068         * 
069         * @author Matej Knopp
070         */
071        protected static final class UrlInfo
072        {
073                private final PageComponentInfo pageComponentInfo;
074                private final PageParameters pageParameters;
075                private final Class<? extends IRequestablePage> pageClass;
076
077                /**
078                 * Construct.
079                 * 
080                 * @param pageComponentInfo
081                 *            optional parameter providing the page instance and component information
082                 * @param pageClass
083                 *            mandatory parameter
084                 * @param pageParameters
085                 *            optional parameter providing pageParameters
086                 */
087                public UrlInfo(PageComponentInfo pageComponentInfo,
088                        Class<? extends IRequestablePage> pageClass, PageParameters pageParameters)
089                {
090                        Args.notNull(pageClass, "pageClass");
091
092                        this.pageComponentInfo = pageComponentInfo;
093                        this.pageParameters = cleanPageParameters(pageParameters);
094
095                        this.pageClass = pageClass;
096                }
097
098                /**
099                 * Cleans the original parameters from entries used by Wicket internals.
100                 * 
101                 * @param originalParameters
102                 *            the current request's non-modified parameters
103                 * @return all parameters but Wicket internal ones
104                 */
105                private PageParameters cleanPageParameters(final PageParameters originalParameters)
106                {
107                        PageParameters cleanParameters = null;
108                        if (originalParameters != null)
109                        {
110                                cleanParameters = new PageParameters(originalParameters);
111
112                                // WICKET-4038: Ajax related parameters are set by wicket-ajax.js when needed.
113                                // They shouldn't be propagated to the next requests
114                                cleanParameters.remove(WebRequest.PARAM_AJAX);
115                                cleanParameters.remove(WebRequest.PARAM_AJAX_BASE_URL);
116                                cleanParameters.remove(WebRequest.PARAM_AJAX_REQUEST_ANTI_CACHE);
117
118                                if (cleanParameters.isEmpty())
119                                {
120                                        cleanParameters = null;
121                                }
122                        }
123                        return cleanParameters;
124                }
125
126                /**
127                 * @return PageComponentInfo instance or <code>null</code>
128                 */
129                public PageComponentInfo getPageComponentInfo()
130                {
131                        return pageComponentInfo;
132                }
133
134                /**
135                 * @return page class
136                 */
137                public Class<? extends IRequestablePage> getPageClass()
138                {
139                        return pageClass;
140                }
141
142                /**
143                 * @return PageParameters instance (never <code>null</code>)
144                 */
145                public PageParameters getPageParameters()
146                {
147                        return pageParameters;
148                }
149        }
150
151        protected final List<MountPathSegment> pathSegments;
152
153        protected final String[] mountSegments;
154
155        protected final IPageParametersEncoder pageParametersEncoder;
156
157        /**
158         * Construct.
159         */
160        public AbstractBookmarkableMapper()
161        {
162                this("notUsed", new PageParametersEncoder());
163        }
164
165        public AbstractBookmarkableMapper(String mountPath, IPageParametersEncoder pageParametersEncoder)
166        {
167                Args.notEmpty(mountPath, "mountPath");
168
169                this.pageParametersEncoder = Args.notNull(pageParametersEncoder, "pageParametersEncoder");
170                mountSegments = getMountSegments(mountPath);
171                pathSegments = getPathSegments(mountSegments);
172        }
173
174        /**
175         * Parse the given request to an {@link UrlInfo} instance.
176         * 
177         * @param request
178         * @return UrlInfo instance or <code>null</code> if this encoder can not handle the request
179         */
180        protected abstract UrlInfo parseRequest(Request request);
181
182        /**
183         * Builds URL for the given {@link UrlInfo} instance. The URL this method produces must be
184         * parseable by the {@link #parseRequest(Request)} method.
185         * 
186         * @param info
187         * @return Url result URL
188         */
189        protected abstract Url buildUrl(UrlInfo info);
190
191        /**
192         * Indicates whether hybrid {@link RenderPageRequestHandler} URL for page will be generated only
193         * if page has been created with bookmarkable URL.
194         * <p>
195         * For generic bookmarkable encoders this method should return <code>true</code>. For explicit
196         * (mounted) encoders this method should return <code>false</code>
197         * 
198         * @return <code>true</code> if hybrid URL requires page created bookmarkable,
199         *         <code>false</code> otherwise.
200         */
201        protected abstract boolean pageMustHaveBeenCreatedBookmarkable();
202
203        @Override
204        public int getCompatibilityScore(Request request)
205        {
206                if (urlStartsWith(request.getUrl(), mountSegments))
207                {
208                        /* see WICKET-5056 - alter score with pathSegment type */
209                        int countOptional = 0;
210                        int fixedSegments = 0;
211                        for (MountPathSegment pathSegment : pathSegments)
212                        {
213                                fixedSegments += pathSegment.getFixedPartSize();
214                                countOptional += pathSegment.getOptionalParameters();
215                        }
216                        return mountSegments.length - countOptional + fixedSegments;
217                }
218                else
219                {
220                        return 0;
221                }
222        }
223
224        /**
225         * Creates a {@code IRequestHandler} that processes a bookmarkable request.
226         * 
227         * @param pageClass
228         * @param pageParameters
229         * @return a {@code IRequestHandler} capable of processing the bookmarkable request.
230         */
231        protected IRequestHandler processBookmarkable(Class<? extends IRequestablePage> pageClass,
232                PageParameters pageParameters)
233        {
234                PageProvider provider = new PageProvider(pageClass, pageParameters);
235                provider.setPageSource(getContext());
236                return new RenderPageRequestHandler(provider);
237        }
238
239        /**
240         * Creates a {@code IRequestHandler} that processes a hybrid request. When the page identified
241         * by {@code pageInfo} was not available, the request should be treated as a bookmarkable
242         * request.
243         * 
244         * @param pageInfo
245         * @param pageClass
246         * @param pageParameters
247         * @param renderCount
248         * @return a {@code IRequestHandler} capable of processing the hybrid request.
249         */
250        protected IRequestHandler processHybrid(PageInfo pageInfo,
251                Class<? extends IRequestablePage> pageClass, PageParameters pageParameters,
252                Integer renderCount)
253        {
254                PageProvider provider = new PageProvider(pageInfo.getPageId(), pageClass, pageParameters,
255                        renderCount);
256                provider.setPageSource(getContext());
257
258                checkExpiration(provider, pageInfo);
259
260                /**
261                 * https://issues.apache.org/jira/browse/WICKET-5734
262                 * */
263                PageParameters constructionPageParameters = provider.hasPageInstance() ?
264                        provider.getPageInstance().getPageParameters() : new PageParameters();
265
266                if (PageParameters.equals(constructionPageParameters, pageParameters) == false)
267                {
268                        // create a fresh page instance because the request page parameters are different than the ones
269                        // when the resolved page by id has been created
270                        return new RenderPageRequestHandler(new PageProvider(pageClass, pageParameters));
271                }
272                return new RenderPageRequestHandler(provider);
273        }
274
275        boolean getRecreateMountedPagesAfterExpiry()
276        {
277                return WebApplication.get().getPageSettings().getRecreateBookmarkablePagesAfterExpiry();
278        }
279
280        /**
281         * Creates a {@code IRequestHandler} that notifies an {@link IRequestListener}.
282         * 
283         * @param pageComponentInfo
284         * @param pageClass
285         * @param pageParameters
286         * @return a {@code IRequestHandler} that notifies an {@link IRequestListener}.
287         */
288        protected IRequestHandler processListener(PageComponentInfo pageComponentInfo,
289                Class<? extends IRequestablePage> pageClass, PageParameters pageParameters)
290        {
291                PageInfo pageInfo = pageComponentInfo.getPageInfo();
292                ComponentInfo componentInfo = pageComponentInfo.getComponentInfo();
293                Integer renderCount = null;
294
295                if (componentInfo != null)
296                {
297                        renderCount = componentInfo.getRenderCount();
298                }
299
300                PageAndComponentProvider provider = new PageAndComponentProvider(pageInfo.getPageId(),
301                        pageClass, pageParameters, renderCount, componentInfo.getComponentPath());
302
303                provider.setPageSource(getContext());
304
305                checkExpiration(provider, pageInfo);
306
307                return new ListenerRequestHandler(provider, componentInfo.getBehaviorId());
308        }
309
310        private void checkExpiration(PageProvider provider, PageInfo pageInfo)
311        {
312                if (provider.wasExpired() && !getRecreateMountedPagesAfterExpiry())
313                {
314                        throw new PageExpiredException(String.format("Bookmarkable page with id '%d' has expired.",
315                                        pageInfo.getPageId()));
316                }
317        }
318
319        @Override
320        public IRequestHandler mapRequest(Request request)
321        {
322                UrlInfo urlInfo = parseRequest(request);
323
324                if (urlInfo != null)
325                {
326                        PageComponentInfo info = urlInfo.getPageComponentInfo();
327                        Class<? extends IRequestablePage> pageClass = urlInfo.getPageClass();
328                        PageParameters pageParameters = urlInfo.getPageParameters();
329
330                        if (info == null)
331                        {
332                                // if there are is no page instance information
333                                // then this is a simple bookmarkable URL
334                                return processBookmarkable(pageClass, pageParameters);
335                        }
336                        else if (info.getPageInfo().getPageId() != null && info.getComponentInfo() == null)
337                        {
338                                // if there is page instance information in the URL but no component and listener
339                                // interface then this is a hybrid URL - we need to try to reuse existing page
340                                // instance
341                                return processHybrid(info.getPageInfo(), pageClass, pageParameters, null);
342                        }
343                        else if (info.getComponentInfo() != null)
344                        {
345                                // with both page instance and component this is a request listener URL
346                                return processListener(info, pageClass, pageParameters);
347                        }
348                        else if (info.getPageInfo().getPageId() == null)
349                        {
350                                return processBookmarkable(pageClass, pageParameters);
351                        }
352
353                }
354                return null;
355        }
356
357        protected boolean checkPageInstance(IRequestablePage page)
358        {
359                return page != null && checkPageClass(page.getClass());
360        }
361
362        protected boolean checkPageClass(Class<? extends IRequestablePage> pageClass)
363        {
364                return true;
365        }
366
367        @Override
368        public Url mapHandler(IRequestHandler requestHandler)
369        {
370                // TODO see if we can refactor this to remove dependency on instanceof checks below and
371                // eliminate the need for IRequestHandlerDelegate
372                while (requestHandler instanceof IRequestHandlerDelegate)
373                {
374                        requestHandler = ((IRequestHandlerDelegate)requestHandler).getDelegateHandler();
375                }
376
377                if (requestHandler instanceof BookmarkablePageRequestHandler)
378                {
379                        // simple bookmarkable URL with no page instance information
380                        BookmarkablePageRequestHandler handler = (BookmarkablePageRequestHandler)requestHandler;
381
382                        if (!checkPageClass(handler.getPageClass()))
383                        {
384                                return null;
385                        }
386
387                        PageInfo info = new PageInfo();
388                        UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(info, null),
389                                handler.getPageClass(), handler.getPageParameters());
390
391                        return buildUrl(urlInfo);
392                }
393                else if (requestHandler instanceof RenderPageRequestHandler)
394                {
395                        // possibly hybrid URL - bookmarkable URL with page instance information
396                        // but only allowed if the page was created by bookmarkable URL
397
398                        RenderPageRequestHandler handler = (RenderPageRequestHandler)requestHandler;
399
400                        if (!checkPageClass(handler.getPageClass()))
401                        {
402                                return null;
403                        }
404
405                        if (!handler.getPageProvider().hasPageInstance())
406                        {
407                                // no existing page instance available, don't bother creating new page instance
408                                PageInfo info = new PageInfo();
409                                UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(info, null),
410                                        handler.getPageClass(), handler.getPageParameters());
411
412                                return buildUrl(urlInfo);
413                        }
414
415                        IRequestablePage page = handler.getPage();
416
417                        if (checkPageInstance(page) &&
418                                (!pageMustHaveBeenCreatedBookmarkable() || page.wasCreatedBookmarkable()))
419                        {
420                                PageInfo info = getPageInfo(handler);
421                                PageComponentInfo pageComponentInfo = new PageComponentInfo(info, null);
422
423                                UrlInfo urlInfo = new UrlInfo(pageComponentInfo, page.getClass(),
424                                        handler.getPageParameters());
425                                return buildUrl(urlInfo);
426                        }
427                        else
428                        {
429                                return null;
430                        }
431
432                }
433                else if (requestHandler instanceof BookmarkableListenerRequestHandler)
434                {
435                        // request listener URL with page class information
436                        BookmarkableListenerRequestHandler handler = (BookmarkableListenerRequestHandler)requestHandler;
437                        Class<? extends IRequestablePage> pageClass = handler.getPageClass();
438
439                        if (!checkPageClass(pageClass))
440                        {
441                                return null;
442                        }
443
444                        Integer renderCount = null;
445                        if (handler.includeRenderCount())
446                        {
447                                renderCount = handler.getRenderCount();
448                        }
449
450                        PageInfo pageInfo = getPageInfo(handler);
451                        ComponentInfo componentInfo = new ComponentInfo(renderCount, handler.getComponentPath(), handler.getBehaviorIndex());
452
453                        PageParameters parameters = getRecreateMountedPagesAfterExpiry() ? new PageParameters(
454                                handler.getPage().getPageParameters()).mergeWith(handler.getPageParameters())
455                                : handler.getPageParameters();
456                        UrlInfo urlInfo = new UrlInfo(new PageComponentInfo(pageInfo, componentInfo),
457                                pageClass, parameters);
458                        return buildUrl(urlInfo);
459                }
460
461                return null;
462        }
463
464        protected final PageInfo getPageInfo(IPageRequestHandler handler)
465        {
466                Args.notNull(handler, "handler");
467
468                Integer pageId = null;
469                if (handler.isPageInstanceCreated())
470                {
471                        IRequestablePage page = handler.getPage();
472
473                        if (page.isPageStateless() == false)
474                        {
475                                pageId = page.getPageId();
476                        }
477                }
478
479                return new PageInfo(pageId);
480        }
481
482        /**
483         * @return a new instance of {@link PageParameters} that will be passed to the page/resource
484         */
485        protected PageParameters newPageParameters()
486        {
487                final PageParameters parameters = new PageParameters();
488                parameters.setLocale(resolveLocale());
489                return parameters;
490        }
491
492        /**
493         * Override {@link #resolveLocale()} to return the result of this method if you want to use
494         * the user's session or request locale for parsing numbers from the page parameters
495         *
496         * @return the Session or Request's locale to use for parsing any numbers in the request parameters
497         */
498        protected Locale resolveUserLocale()
499        {
500                Locale locale = super.resolveLocale();
501                if (Session.exists())
502                {
503                        locale = Session.get().getLocale();
504                }
505                else
506                {
507                        RequestCycle requestCycle = RequestCycle.get();
508                        if (requestCycle != null)
509                        {
510                                Request request = requestCycle.getRequest();
511                                if (request != null)
512                                {
513                                        locale = request.getLocale();
514                                }
515                        }
516                }
517
518                return locale;
519        }
520
521        protected static class MountPathSegment
522        {
523                private int segmentIndex;
524                private String fixedPart;
525                private int minParameters;
526                private int optionalParameters;
527
528                public MountPathSegment(int segmentIndex)
529                {
530                        this.segmentIndex = segmentIndex;
531                }
532
533                public void setFixedPart(String fixedPart)
534                {
535                        this.fixedPart = fixedPart;
536                }
537
538                public void addRequiredParameter()
539                {
540                        minParameters++;
541                }
542
543                public void addOptionalParameter()
544                {
545                        optionalParameters++;
546                }
547
548                public int getSegmentIndex()
549                {
550                        return segmentIndex;
551                }
552
553                public String getFixedPart()
554                {
555                        return fixedPart;
556                }
557
558                public int getMinParameters()
559                {
560                        return minParameters;
561                }
562
563                public int getOptionalParameters()
564                {
565                        return optionalParameters;
566                }
567
568                public int getMaxParameters()
569                {
570                        return getOptionalParameters() + getMinParameters();
571                }
572
573                public int getFixedPartSize()
574                {
575                        return getFixedPart() == null ? 0 : 1;
576                }
577
578                @Override
579                public String toString()
580                {
581                        return "(" + getSegmentIndex() + ") " + getMinParameters() + '-' + getMaxParameters() +
582                                        ' ' + (getFixedPart() == null ? "(end)" : getFixedPart());
583                }
584        }
585
586        protected List<MountPathSegment> getPathSegments(String[] segments)
587        {
588                List<MountPathSegment> ret = new ArrayList<MountPathSegment>();
589                int segmentIndex = 0;
590                MountPathSegment curPathSegment = new MountPathSegment(segmentIndex);
591                ret.add(curPathSegment);
592                for (String curSegment : segments)
593                {
594                        if (isFixedSegment(curSegment))
595                        {
596                                curPathSegment.setFixedPart(curSegment);
597                                curPathSegment = new MountPathSegment(segmentIndex + 1);
598                                ret.add(curPathSegment);
599                        }
600                        else if (getPlaceholder(curSegment) != null)
601                        {
602                                curPathSegment.addRequiredParameter();
603                        }
604                        else
605                        {
606                                curPathSegment.addOptionalParameter();
607                        }
608                        segmentIndex++;
609                }
610                return ret;
611        }
612
613        protected boolean isFixedSegment(String segment)
614        {
615                return getOptionalPlaceholder(segment) == null && getPlaceholder(segment) == null;
616        }
617
618
619        /**
620         * Extracts the PageParameters from URL if there are any
621         */
622        protected PageParameters extractPageParameters(Request request, Url url)
623        {
624                int[] matchedParameters = getMatchedSegmentSizes(url);
625                
626                int total = 0;
627                for (int curMatchSize : matchedParameters)
628                {
629                        total += curMatchSize;
630                }
631                PageParameters pageParameters = extractPageParameters(request, total, pageParametersEncoder);
632                if (pageParameters != null)
633                {
634                        pageParameters.setLocale(resolveLocale());
635                }
636
637                int segmentIndex = 0;
638                for (int pathSegmentIndex = 0; pathSegmentIndex < pathSegments.size(); pathSegmentIndex++)
639                {
640                        MountPathSegment pathSegment = pathSegments.get(pathSegmentIndex);
641                        
642                        int totalAdded = 0;
643                        int requiredAdded = 0;
644                        for (int segmentParameterIndex = 0; segmentParameterIndex < pathSegment.getMaxParameters() && totalAdded < matchedParameters[pathSegmentIndex]; segmentParameterIndex++)
645                        {
646                                if (pageParameters == null)
647                                {
648                                        pageParameters = newPageParameters();
649                                }
650
651                                String curSegment = mountSegments[pathSegment.getSegmentIndex() + segmentParameterIndex];
652
653                                String placeholder = getPlaceholder(curSegment);
654                                String optionalPlaceholder = getOptionalPlaceholder(curSegment);
655                                // extract the parameter from URL
656                                if (placeholder != null)
657                                {
658                                        pageParameters.add(placeholder,
659                                                        url.getSegments().get(segmentIndex), INamedParameters.Type.PATH);
660                                        segmentIndex++;
661                                        totalAdded++;
662                                        requiredAdded++;
663                                }
664                                else if (optionalPlaceholder != null &&
665                                        matchedParameters[pathSegmentIndex] - segmentParameterIndex > pathSegment.getMinParameters() + pathSegment.getFixedPartSize() - requiredAdded)
666                                {
667                                        pageParameters.add(optionalPlaceholder,
668                                                        url.getSegments().get(segmentIndex), INamedParameters.Type.PATH);
669                                        segmentIndex++;
670                                        totalAdded++;
671                                }
672                        }
673
674                        segmentIndex += pathSegment.getFixedPartSize();
675                }
676                return pageParameters;
677        }
678
679        protected int[] getMatchedSegmentSizes(Url url)
680        {
681                int[] ret = new int[pathSegments.size()];
682                int segmentIndex = 0;
683                int pathSegmentIndex = 0;
684                for (MountPathSegment curPathSegment : pathSegments.subList(0, pathSegments.size() - 1))
685                {
686                        boolean foundFixedPart = false;
687                        segmentIndex += curPathSegment.getMinParameters();
688                        int max = Math.min(curPathSegment.getOptionalParameters() + 1,
689                                        url.getSegments().size() - segmentIndex);
690
691                        for (int count = max - 1; count >= 0; count--)
692                        {
693                                if (segmentsMatch(url.getSegments()
694                                                .get(segmentIndex + count), curPathSegment.getFixedPart()))
695                                {
696                                        foundFixedPart = true;
697                                        segmentIndex += count + 1;
698                                        ret[pathSegmentIndex] = count + curPathSegment.getMinParameters() + 1;
699                                        break;
700                                }
701                        }
702                        if (!foundFixedPart)
703                                return null;
704                        pathSegmentIndex++;
705                }
706                MountPathSegment lastSegment = pathSegments.get(pathSegments.size() - 1);
707                segmentIndex += lastSegment.getMinParameters();
708                if (segmentIndex > url.getSegments().size())
709                        return null;
710                ret[pathSegmentIndex] = Math.min(lastSegment.getMaxParameters(), url.getSegments().size() -
711                                segmentIndex + lastSegment.getMinParameters());
712                return ret;
713        }
714
715        /**
716         * Decides whether a segment from the mounted path matches with a segment
717         * from the requested url.
718         *
719         * A custom implementation of this class may use more complex logic to handle
720         * spelling errors
721         *
722         * @param mountedSegment
723         *          the segment from the mounted path
724         * @param urlSegment
725         *          the segment from the request url
726         * @return {@code true} if the segments match
727         */
728        protected boolean segmentsMatch(String mountedSegment, String urlSegment)
729        {
730                final boolean result;
731                if (isCaseSensitiveMatch())
732                {
733                        result = mountedSegment.equals(urlSegment);
734                }
735                else
736                {
737                        result = mountedSegment.equalsIgnoreCase(urlSegment);
738                }
739                return result;
740        }
741
742        /**
743         * @return whether the matching of mounted segments against request's url ones should be
744         *      case sensitive or not
745         */
746        protected boolean isCaseSensitiveMatch()
747        {
748                return isCaseSensitive;
749        }
750
751        /**
752         * Sets whether the matching of mounted segments against request's url ones should be
753         * case sensitive or not.
754         *
755         * @param isCaseSensitive
756         *          a flag indicating whether the matching of mounted segments against request's
757         *          url ones should be case sensitive or not
758         * @return this instance, for chaining
759         */
760        public AbstractBookmarkableMapper setCaseSensitiveMatch(boolean isCaseSensitive)
761        {
762                this.isCaseSensitive = isCaseSensitive;
763                return this;
764        }
765
766        /**
767         * Replaces mandatory and optional parameters with their values.
768         *
769         * If a mandatory parameter is not provided then the method returns {@code false}
770         * indicating that there is a problem.
771         * Optional parameters with missing values are just dropped.
772         *
773         * @param parameters
774         *          The parameters with the values
775         * @param url
776         *          The url with the placeholders
777         * @return
778         *          {@code true} if all mandatory parameters are properly substituted,
779         *          {@code false} - otherwise
780         */
781        protected boolean setPlaceholders(PageParameters parameters, Url url)
782        {
783                boolean mandatoryParametersSet = true;
784
785                int dropped = 0;
786                for (int i = 0; i < mountSegments.length; ++i)
787                {
788                        String placeholder = getPlaceholder(mountSegments[i]);
789                        String optionalPlaceholder = getOptionalPlaceholder(mountSegments[i]);
790                        if (placeholder != null)
791                        {
792                                if (parameters.contains(placeholder))
793                                {
794                                        url.getSegments().set(i - dropped, parameters.get(placeholder).toString());
795                                        parameters.remove(placeholder);
796                                }
797                                else
798                                {
799                                        mandatoryParametersSet = false;
800                                        break;
801                                }
802                        }
803                        else if (optionalPlaceholder != null)
804                        {
805                                if (parameters.contains(optionalPlaceholder))
806                                {
807                                        url.getSegments().set(i - dropped, parameters.get(optionalPlaceholder).toString(""));
808                                        parameters.remove(optionalPlaceholder);
809                                }
810                                else
811                                {
812                                        url.getSegments().remove(i - dropped);
813                                        dropped++;
814                                }
815                        }
816                }
817
818                return mandatoryParametersSet;
819        }
820        
821        protected boolean urlStartsWithMountedSegments(Url url)
822        {
823                if (url == null)
824                {
825                        return false;
826                }
827                else
828                {
829                        return getMatchedSegmentSizes(url) != null;
830                }
831        }
832}