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.camel.support;
018
019import java.util.ArrayList;
020import java.util.Comparator;
021import java.util.Iterator;
022import java.util.List;
023import java.util.Locale;
024
025/**
026 * A context path matcher when using rest-dsl that allows components to reuse the same matching logic.
027 * <p/>
028 * The component should use the {@link #matchBestPath(String, String, java.util.List)} with the request details
029 * and the matcher returns the best matched, or <tt>null</tt> if none could be determined.
030 * <p/>
031 * The {@link ConsumerPath} is used for the components to provide the details to the matcher.
032 */
033public final class RestConsumerContextPathMatcher {
034
035    private RestConsumerContextPathMatcher() {
036    }
037    
038    /**
039     * Consumer path details which must be implemented and provided by the components.
040     */
041    public interface ConsumerPath<T> {
042
043        /**
044         * Any HTTP restrict method that would not be allowed
045         */
046        String getRestrictMethod();
047
048        /**
049         * The consumer context-path which may include wildcards
050         */
051        String getConsumerPath();
052
053        /**
054         * The consumer implementation
055         */
056        T getConsumer();
057
058        /**
059         * Whether the consumer match on uri prefix
060         */
061        boolean isMatchOnUriPrefix();
062
063    }
064
065    /**
066     * Does the incoming request match the given consumer path (ignore case)
067     *
068     * @param requestPath      the incoming request context path
069     * @param consumerPath     a consumer path
070     * @param matchOnUriPrefix whether to use the matchOnPrefix option
071     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
072     */
073    public static boolean matchPath(String requestPath, String consumerPath, boolean matchOnUriPrefix) {
074        // deal with null parameters
075        if (requestPath == null && consumerPath == null) {
076            return true;
077        }
078        if (requestPath == null || consumerPath == null) {
079            return false;
080        }
081
082        // remove starting/ending slashes
083        if (requestPath.startsWith("/")) {
084            requestPath = requestPath.substring(1);
085        }
086        if (requestPath.endsWith("/")) {
087            requestPath = requestPath.substring(0, requestPath.length() - 1);
088        }
089        // remove starting/ending slashes
090        if (consumerPath.startsWith("/")) {
091            consumerPath = consumerPath.substring(1);
092        }
093        if (consumerPath.endsWith("/")) {
094            consumerPath = consumerPath.substring(0, consumerPath.length() - 1);
095        }
096
097        if (matchOnUriPrefix && requestPath.toLowerCase(Locale.ENGLISH).startsWith(consumerPath.toLowerCase(Locale.ENGLISH))) {
098            return true;
099        }
100        
101        if (requestPath.equalsIgnoreCase(consumerPath)) {
102            return true;
103        }
104        
105        return false;
106    }
107
108    /**
109     * Finds the best matching of the list of consumer paths that should service the incoming request.
110     *
111     * @param requestMethod the incoming request HTTP method
112     * @param requestPath   the incoming request context path
113     * @param consumerPaths the list of consumer context path details
114     * @return the best matched consumer, or <tt>null</tt> if none could be determined.
115     */
116    public static ConsumerPath matchBestPath(String requestMethod, String requestPath, List<ConsumerPath> consumerPaths) {
117        ConsumerPath answer = null;
118
119        List<ConsumerPath> candidates = new ArrayList<>();
120
121        // first match by http method
122        for (ConsumerPath entry : consumerPaths) {
123            if (matchRestMethod(requestMethod, entry.getRestrictMethod())) {
124                candidates.add(entry);
125            }
126        }
127
128        // then see if we got a direct match
129        Iterator<ConsumerPath> it = candidates.iterator();
130        while (it.hasNext()) {
131            ConsumerPath consumer = it.next();
132            if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) {
133                answer = consumer;
134                break;
135            }
136        }
137
138        // we could not find a direct match, and if the request is OPTIONS then we need all candidates
139        if (answer == null && isOptionsMethod(requestMethod)) {
140            candidates.clear();
141            candidates.addAll(consumerPaths);
142
143            // then try again to see if we can find a direct match
144            it = candidates.iterator();
145            while (it.hasNext()) {
146                ConsumerPath consumer = it.next();
147                if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) {
148                    answer = consumer;
149                    break;
150                }
151            }
152        }
153
154        // if there are no wildcards, then select the matching with the longest path
155        boolean noWildcards = candidates.stream().allMatch(p -> countWildcards(p.getConsumerPath()) == 0);
156        if (noWildcards) {
157            // grab first which is the longest that matched the request path
158            answer = candidates.stream()
159                .filter(c -> matchPath(requestPath, c.getConsumerPath(), c.isMatchOnUriPrefix()))
160                // sort by longest by inverting the sort by multiply with -1
161                .sorted(Comparator.comparingInt(o -> -1 * o.getConsumerPath().length())).findFirst().orElse(null);
162        }
163
164        // then match by wildcard path
165        if (answer == null) {
166            it = candidates.iterator();
167            while (it.hasNext()) {
168                ConsumerPath consumer = it.next();
169                // filter non matching paths
170                if (!matchRestPath(requestPath, consumer.getConsumerPath(), true)) {
171                    it.remove();
172                }
173            }
174
175            // if there is multiple candidates with wildcards then pick anyone with the least number of wildcards
176            int bestWildcard = Integer.MAX_VALUE;
177            ConsumerPath best = null;
178            if (candidates.size() > 1) {
179                it = candidates.iterator();
180                while (it.hasNext()) {
181                    ConsumerPath entry = it.next();
182                    int wildcards = countWildcards(entry.getConsumerPath());
183                    if (wildcards > 0) {
184                        if (best == null || wildcards < bestWildcard) {
185                            best = entry;
186                            bestWildcard = wildcards;
187                        }
188                    }
189                }
190
191                if (best != null) {
192                    // pick the best among the wildcards
193                    answer = best;
194                }
195            }
196
197            // if there is one left then its our answer
198            if (answer == null && candidates.size() == 1) {
199                answer = candidates.get(0);
200            }
201        }
202
203        return answer;
204    }
205
206    /**
207     * Matches the given request HTTP method with the configured HTTP method of the consumer.
208     *
209     * @param method   the request HTTP method
210     * @param restrict the consumer configured HTTP restrict method
211     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
212     */
213    private static boolean matchRestMethod(String method, String restrict) {
214        if (restrict == null) {
215            return true;
216        }
217
218        return restrict.toLowerCase(Locale.ENGLISH).contains(method.toLowerCase(Locale.ENGLISH));
219    }
220
221    /**
222     * Is the request method OPTIONS
223     *
224     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
225     */
226    private static boolean isOptionsMethod(String method) {
227        return "options".equalsIgnoreCase(method);
228    }
229
230    /**
231     * Matches the given request path with the configured consumer path
232     *
233     * @param requestPath  the request path
234     * @param consumerPath the consumer path which may use { } tokens
235     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
236     */
237    private static boolean matchRestPath(String requestPath, String consumerPath, boolean wildcard) {
238        // deal with null parameters
239        if (requestPath == null && consumerPath == null) {
240            return true;
241        }
242        if (requestPath == null || consumerPath == null) {
243            return false;
244        }
245
246        // remove starting/ending slashes
247        if (requestPath.startsWith("/")) {
248            requestPath = requestPath.substring(1);
249        }
250        if (requestPath.endsWith("/")) {
251            requestPath = requestPath.substring(0, requestPath.length() - 1);
252        }
253        // remove starting/ending slashes
254        if (consumerPath.startsWith("/")) {
255            consumerPath = consumerPath.substring(1);
256        }
257        if (consumerPath.endsWith("/")) {
258            consumerPath = consumerPath.substring(0, consumerPath.length() - 1);
259        }
260
261        // split using single char / is optimized in the jdk
262        String[] requestPaths = requestPath.split("/");
263        String[] consumerPaths = consumerPath.split("/");
264
265        // must be same number of path's
266        if (requestPaths.length != consumerPaths.length) {
267            return false;
268        }
269
270        for (int i = 0; i < requestPaths.length; i++) {
271            String p1 = requestPaths[i];
272            String p2 = consumerPaths[i];
273
274            if (wildcard && p2.startsWith("{") && p2.endsWith("}")) {
275                // always matches
276                continue;
277            }
278
279            if (!matchPath(p1, p2, false)) {
280                return false;
281            }
282        }
283
284        // assume matching
285        return true;
286    }
287
288    /**
289     * Counts the number of wildcards in the path
290     *
291     * @param consumerPath the consumer path which may use { } tokens
292     * @return number of wildcards, or <tt>0</tt> if no wildcards
293     */
294    private static int countWildcards(String consumerPath) {
295        int wildcards = 0;
296
297        // remove starting/ending slashes
298        if (consumerPath.startsWith("/")) {
299            consumerPath = consumerPath.substring(1);
300        }
301        if (consumerPath.endsWith("/")) {
302            consumerPath = consumerPath.substring(0, consumerPath.length() - 1);
303        }
304
305        String[] consumerPaths = consumerPath.split("/");
306        for (String p2 : consumerPaths) {
307            if (p2.startsWith("{") && p2.endsWith("}")) {
308                wildcards++;
309            }
310        }
311
312        return wildcards;
313    }
314
315}