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.request.mapper;
018
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Iterator;
022import java.util.List;
023import java.util.concurrent.CopyOnWriteArrayList;
024
025import org.apache.wicket.request.IRequestHandler;
026import org.apache.wicket.request.IRequestMapper;
027import org.apache.wicket.request.Request;
028import org.apache.wicket.request.Url;
029import org.slf4j.Logger;
030import org.slf4j.LoggerFactory;
031
032
033/**
034 * Thread safe compound {@link IRequestMapper}. The mappers are searched depending on their
035 * compatibility score and the orders they were registered. If two or more {@link IRequestMapper}s
036 * have the same compatibility score, the last registered mapper has highest priority.
037 * 
038 * @author igor.vaynberg
039 * @author Matej Knopp
040 */
041public class CompoundRequestMapper implements ICompoundRequestMapper
042{
043        private static final Logger LOG = LoggerFactory.getLogger(CompoundRequestMapper.class);
044
045        static class MapperWithScore implements Comparable<MapperWithScore>
046        {
047                private final IRequestMapper mapper;
048                private final int compatibilityScore;
049
050                public MapperWithScore(final IRequestMapper mapper, final int compatibilityScore)
051                {
052                        this.mapper = mapper;
053                        this.compatibilityScore = compatibilityScore;
054                }
055
056                @Override
057                public int compareTo(final MapperWithScore o)
058                {
059                        return (compatibilityScore < o.compatibilityScore ? 1
060                                : (compatibilityScore > o.compatibilityScore ? -1 : 0));
061                }
062
063                public IRequestMapper getMapper()
064                {
065                        return mapper;
066                }
067
068                @Override
069                public boolean equals(Object o)
070                {
071                        if (this == o)
072                                return true;
073                        if (!(o instanceof MapperWithScore))
074                                return false;
075
076                        MapperWithScore that = (MapperWithScore)o;
077
078                        if (compatibilityScore != that.compatibilityScore)
079                                return false;
080                        return mapper.equals(that.mapper);
081                }
082
083                @Override
084                public int hashCode()
085                {
086                        int result = mapper.hashCode();
087                        result = 31 * result + compatibilityScore;
088                        return result;
089                }
090
091                @Override
092                public String toString()
093                {
094                        return "Mapper: " + mapper.getClass().getName() + "; Score: " + compatibilityScore;
095                }
096        }
097
098        private final List<IRequestMapper> mappers = new CopyOnWriteArrayList<>();
099
100        @Override
101        public CompoundRequestMapper add(final IRequestMapper mapper)
102        {
103                mappers.add(0, mapper);
104                return this;
105        }
106
107        @Override
108        public CompoundRequestMapper remove(final IRequestMapper mapper)
109        {
110                mappers.remove(mapper);
111                return this;
112        }
113
114        /**
115         * Searches the registered {@link IRequestMapper}s to find one that can map the {@link Request}.
116         * Each registered {@link IRequestMapper} is asked to provide its compatibility score. Then the
117         * mappers are asked to map the request in order depending on the provided compatibility
118         * score.
119         * <p>
120         * The mapper with highest compatibility score which can map the request is returned.
121         * 
122         * @param request
123         * @return RequestHandler for the request or <code>null</code> if no mapper for the request is
124         *         found.
125         */
126        @Override
127        public IRequestHandler mapRequest(final Request request)
128        {
129                List<MapperWithScore> list = new ArrayList<>(mappers.size());
130
131                for (IRequestMapper mapper : this)
132                {
133                        int score = mapper.getCompatibilityScore(request);
134                        list.add(new MapperWithScore(mapper, score));
135                }
136
137                Collections.sort(list);
138
139                if (LOG.isDebugEnabled())
140                {
141                        logMappers(list, request.getUrl().toString());
142                }
143
144                for (MapperWithScore mapperWithScore : list)
145                {
146                        IRequestMapper mapper = mapperWithScore.getMapper();
147                        IRequestHandler handler = mapper.mapRequest(request);
148                        if (handler != null)
149                        {
150                                return handler;
151                        }
152                }
153
154                return null;
155        }
156
157        /**
158         * Logs all mappers with a positive compatibility score
159         *
160         * @param mappersWithScores
161         *      the list of all mappers
162         * @param url
163         *      the url to match by these mappers
164         */
165        private void logMappers(final List<MapperWithScore> mappersWithScores, final String url)
166        {
167                final List<MapperWithScore> compatibleMappers = new ArrayList<>();
168                for (MapperWithScore mapperWithScore : mappersWithScores)
169                {
170                        if (mapperWithScore.compatibilityScore > 0)
171                        {
172                                compatibleMappers.add(mapperWithScore);
173                        }
174                }
175                if (compatibleMappers.size() == 0)
176                {
177                        LOG.debug("No compatible mapper found for URL '{}'", url);
178                }
179                else if (compatibleMappers.size() == 1)
180                {
181                        LOG.debug("One compatible mapper found for URL '{}' -> '{}'", url, compatibleMappers.get(0));
182                }
183                else
184                {
185                        LOG.debug("Multiple compatible mappers found for URL '{}'", url);
186                        for (MapperWithScore compatibleMapper : compatibleMappers)
187                        {
188                        LOG.debug(" * {}", compatibleMapper);
189                        }
190                }
191        }
192
193        /**
194         * Searches the registered {@link IRequestMapper}s to find one that can map the
195         * {@link IRequestHandler}. Each registered {@link IRequestMapper} is asked to map the
196         * {@link IRequestHandler} until a mapper which can map the {@link IRequestHandler} is found or
197         * no more mappers are left.
198         * <p>
199         * The mappers are searched in reverse order as they have been registered. More recently
200         * registered mappers have bigger priority.
201         * 
202         * @param handler
203         * @return Url for the handler or <code>null</code> if no mapper for the handler is found.
204         */
205        @Override
206        public Url mapHandler(final IRequestHandler handler)
207        {
208                for (IRequestMapper mapper : this)
209                {
210                        Url url = mapper.mapHandler(handler);
211                        if (url != null)
212                        {
213                                return url;
214                        }
215                }
216                return null;
217        }
218
219        /**
220         * The scope of the compound mapper is the highest score of the registered mappers.
221         * 
222         * {@inheritDoc}
223         */
224        @Override
225        public int getCompatibilityScore(final Request request)
226        {
227                int score = Integer.MIN_VALUE;
228                for (IRequestMapper mapper : this)
229                {
230                        score = Math.max(score, mapper.getCompatibilityScore(request));
231                }
232                return score;
233        }
234
235        @Override
236        public Iterator<IRequestMapper> iterator()
237        {
238                return mappers.iterator();
239        }
240}