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.Iterator;
021import java.util.List;
022import java.util.Locale;
023
024/**
025 * A context path matcher when using rest-dsl that allows components to reuse the same matching logic.
026 * <p/>
027 * The component should use the {@link #matchBestPath(String, String, java.util.List)} with the request details
028 * and the matcher returns the best matched, or <tt>null</tt> if none could be determined.
029 * <p/>
030 * The {@link ConsumerPath} is used for the components to provide the details to the matcher.
031 */
032public final class RestConsumerContextPathMatcher {
033    private RestConsumerContextPathMatcher() {
034        
035    }
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
060    /**
061     * Does the incoming request match the given consumer path (ignore case)
062     *
063     * @param requestPath      the incoming request context path
064     * @param consumerPath     a consumer path
065     * @param matchOnUriPrefix whether to use the matchOnPrefix option
066     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
067     */
068    public static boolean matchPath(String requestPath, String consumerPath, boolean matchOnUriPrefix) {
069        // deal with null parameters
070        if (requestPath == null && consumerPath == null) {
071            return true;
072        }
073        if (requestPath == null || consumerPath == null) {
074            return false;
075        }
076
077        String p1 = requestPath.toLowerCase(Locale.ENGLISH);
078        String p2 = consumerPath.toLowerCase(Locale.ENGLISH);
079
080        if (p1.equals(p2)) {
081            return true;
082        }
083
084        if (matchOnUriPrefix && p1.startsWith(p2)) {
085            return true;
086        }
087
088        return false;
089    }
090
091    /**
092     * Finds the best matching of the list of consumer paths that should service the incoming request.
093     *
094     * @param requestMethod the incoming request HTTP method
095     * @param requestPath   the incoming request context path
096     * @param consumerPaths the list of consumer context path details
097     * @return the best matched consumer, or <tt>null</tt> if none could be determined.
098     */
099    public static ConsumerPath matchBestPath(String requestMethod, String requestPath, List<ConsumerPath> consumerPaths) {
100        ConsumerPath answer = null;
101
102        List<ConsumerPath> candidates = new ArrayList<ConsumerPath>();
103
104        // first match by http method
105        for (ConsumerPath entry : consumerPaths) {
106            if (matchRestMethod(requestMethod, entry.getRestrictMethod())) {
107                candidates.add(entry);
108            }
109        }
110
111        // then see if we got a direct match
112        Iterator<ConsumerPath> it = candidates.iterator();
113        while (it.hasNext()) {
114            ConsumerPath consumer = it.next();
115            if (matchRestPath(requestPath, consumer.getConsumerPath(), false)) {
116                answer = consumer;
117                break;
118            }
119        }
120
121        // then match by wildcard path
122        if (answer == null) {
123            it = candidates.iterator();
124            while (it.hasNext()) {
125                ConsumerPath consumer = it.next();
126                // filter non matching paths
127                if (!matchRestPath(requestPath, consumer.getConsumerPath(), true)) {
128                    it.remove();
129                }
130            }
131
132            // if there is multiple candidates with wildcards then pick anyone with the least number of wildcards
133            int bestWildcard = Integer.MAX_VALUE;
134            ConsumerPath best = null;
135            if (candidates.size() > 1) {
136                it = candidates.iterator();
137                while (it.hasNext()) {
138                    ConsumerPath entry = it.next();
139                    int wildcards = countWildcards(entry.getConsumerPath());
140                    if (wildcards > 0) {
141                        if (best == null || wildcards < bestWildcard) {
142                            best = entry;
143                            bestWildcard = wildcards;
144                        }
145                    }
146                }
147
148                if (best != null) {
149                    // pick the best among the wildcards
150                    answer = best;
151                }
152            }
153
154            // if there is one left then its our answer
155            if (answer == null && candidates.size() == 1) {
156                answer = candidates.get(0);
157            }
158        }
159
160        return answer;
161    }
162
163    /**
164     * Matches the given request HTTP method with the configured HTTP method of the consumer
165     *
166     * @param method   the request HTTP method
167     * @param restrict the consumer configured HTTP restrict method
168     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
169     */
170    private static boolean matchRestMethod(String method, String restrict) {
171        if (restrict == null) {
172            return true;
173        }
174
175        return restrict.toLowerCase(Locale.ENGLISH).contains(method.toLowerCase(Locale.ENGLISH));
176    }
177
178    /**
179     * Matches the given request path with the configured consumer path
180     *
181     * @param requestPath  the request path
182     * @param consumerPath the consumer path which may use { } tokens
183     * @return <tt>true</tt> if matched, <tt>false</tt> otherwise
184     */
185    private static boolean matchRestPath(String requestPath, String consumerPath, boolean wildcard) {
186        // remove starting/ending slashes
187        if (requestPath.startsWith("/")) {
188            requestPath = requestPath.substring(1);
189        }
190        if (requestPath.endsWith("/")) {
191            requestPath = requestPath.substring(0, requestPath.length() - 1);
192        }
193        // remove starting/ending slashes
194        if (consumerPath.startsWith("/")) {
195            consumerPath = consumerPath.substring(1);
196        }
197        if (consumerPath.endsWith("/")) {
198            consumerPath = consumerPath.substring(0, consumerPath.length() - 1);
199        }
200
201        // split using single char / is optimized in the jdk
202        String[] requestPaths = requestPath.split("/");
203        String[] consumerPaths = consumerPath.split("/");
204
205        // must be same number of path's
206        if (requestPaths.length != consumerPaths.length) {
207            return false;
208        }
209
210        for (int i = 0; i < requestPaths.length; i++) {
211            String p1 = requestPaths[i];
212            String p2 = consumerPaths[i];
213
214            if (wildcard && p2.startsWith("{") && p2.endsWith("}")) {
215                // always matches
216                continue;
217            }
218
219            if (!matchPath(p1, p2, false)) {
220                return false;
221            }
222        }
223
224        // assume matching
225        return true;
226    }
227
228    /**
229     * Counts the number of wildcards in the path
230     *
231     * @param consumerPath the consumer path which may use { } tokens
232     * @return number of wildcards, or <tt>0</tt> if no wildcards
233     */
234    private static int countWildcards(String consumerPath) {
235        int wildcards = 0;
236
237        // remove starting/ending slashes
238        if (consumerPath.startsWith("/")) {
239            consumerPath = consumerPath.substring(1);
240        }
241        if (consumerPath.endsWith("/")) {
242            consumerPath = consumerPath.substring(0, consumerPath.length() - 1);
243        }
244
245        String[] consumerPaths = consumerPath.split("/");
246        for (String p2 : consumerPaths) {
247            if (p2.startsWith("{") && p2.endsWith("}")) {
248                wildcards++;
249            }
250        }
251
252        return wildcards;
253    }
254
255}