001/*
002 * JGrapes Event Driven Framework
003 * Copyright (C) 2016-2018 Michael N. Lipp
004 * 
005 * This program is free software; you can redistribute it and/or modify it 
006 * under the terms of the GNU Affero General Public License as published by 
007 * the Free Software Foundation; either version 3 of the License, or 
008 * (at your option) any later version.
009 * 
010 * This program is distributed in the hope that it will be useful, but 
011 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
012 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License 
013 * for more details.
014 * 
015 * You should have received a copy of the GNU Affero General Public License along 
016 * with this program; if not, see <http://www.gnu.org/licenses/>.
017 */
018
019package org.jgrapes.http;
020
021import java.net.URI;
022import java.text.ParseException;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.List;
026import java.util.Optional;
027import java.util.Spliterators.AbstractSpliterator;
028import java.util.StringTokenizer;
029import java.util.function.Consumer;
030import java.util.regex.Matcher;
031import java.util.regex.Pattern;
032import java.util.stream.Collectors;
033import java.util.stream.Stream;
034import java.util.stream.StreamSupport;
035
036/**
037 * A resource pattern can be used to filter URIs. A pattern looks  
038 * similar to a URI (<code>scheme://host:port/path</code>) with the 
039 * following differences:
040 * 
041 *  * The <em>scheme</em> may be a single protocol name or a list
042 *    of protocol names separated by commas, or an asterisk, which is matched
043 *    by URIs with any scheme. The scheme part ({@code scheme://}) is optional 
044 *    in a pattern. Omitting it is equivalent to specifying an asterisk.
045 *    
046 *  * The <em>host</em> may be a host name or an IP address or an asterisk.
047 *    Unless the value is an asterisk, filtered URIs must match the value
048 *    literally.
049 *    
050 *  * The <em>port</em> may be a number or an asterisk.
051 *    Unless the value is an asterisk, filtered URIs must match it.
052 *   
053 *  * Specifying a port ({@code :port}) is optional. If omitted, 
054 *    it is equivalent to specifying an asterisk.
055 *    
056 *  * If the scheme part is omitted, the {@code host:port} part may 
057 *    completely be left out as well, which is
058 *    equivalent to specifying an asterisk for both host and port.
059 *    
060 *  * The optional path part consist of one or more path separated by commas.
061 *  
062 *    Each path consists of a sequence of names and asterisks separated
063 *    by slashes or a vertical bar ("|"). A name must be matched by the 
064 *    corresponding path element of filtered URIs, an asterisk is matched by 
065 *    any corresponding path element (which, however, must exist in the 
066 *    filtered URI). The final element in 
067 *    the path of a pattern may be two asterisks ({@code **}), which matches 
068 *    any remaining path elements in the filtered URI.
069 *    
070 *    Using a vertical bar instead of a slash separates the path in a
071 *    prefix part and the rest. The number of prefix segments is the
072 *    value returned by the match methods.
073 *    
074 *    If a path ends with a vertical bar and the URI matched with the
075 *    path does not end with a slash, a slash is appended to the URI
076 *    before matching. This causes both `/foo` and `/foo/` to match
077 *    a path `/foo|`. This is usually intended. If you do want to
078 *    treat `/foo` as a leaf, specify `/foo,/foo|` in your pattern.
079 *
080 */
081@SuppressWarnings("PMD.GodClass")
082public class ResourcePattern {
083
084    @SuppressWarnings("PMD.AvoidFieldNameMatchingTypeName")
085    private static Pattern resourcePattern = Pattern.compile(
086        "^((?<proto>[^:]+|\\*)://)?" // Optional protocol (2)
087            + "(" // Start of optional host/port part
088            + "(?<host>([^\\[/\\|][^:/\\|]*)|(\\[[^\\]]*\\]))" // Host (4)
089            + "(:(?<port>([0-9]+)|(\\*)))?" // Optional port (8)
090            + ")?" // End of optional host/port
091            + "(?<path>[/\\|].*)?"); // Finally path (11)
092
093    private final String pattern;
094    private final String protocol;
095    private final String host;
096    private final String port;
097    private final String path;
098    private final String[][] pathPatternElements;
099    private int[] prefixSegs;
100
101    /**
102     * Creates a new resource pattern.
103     * 
104     * @param pattern the pattern to be used for matching
105     * @throws ParseException if an invalid pattern is specified
106     */
107    @SuppressWarnings({ "PMD.AvoidInstantiatingObjectsInLoops",
108        "PMD.CognitiveComplexity" })
109    public ResourcePattern(String pattern) throws ParseException {
110        this.pattern = pattern;
111        Matcher rpm = resourcePattern.matcher(pattern);
112        if (!rpm.matches()) {
113            throw new ParseException("Invalid pattern: " + pattern, 0);
114        }
115        protocol = rpm.group("proto");
116        host = rpm.group("host");
117        port = rpm.group("port");
118        path = rpm.group("path");
119        if (path == null) {
120            pathPatternElements = new String[0][];
121        } else {
122            String[] paths = path.split(",");
123            pathPatternElements = new String[paths.length][];
124            prefixSegs = new int[paths.length];
125            for (int i = 0; i < paths.length; i++) {
126                @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
127                List<String> segs = new ArrayList<>();
128                prefixSegs[i] = 0;
129                StringTokenizer tokenizer = new StringTokenizer(
130                    paths[i], "/|", true);
131                while (tokenizer.hasMoreTokens()) {
132                    String token = tokenizer.nextToken();
133                    switch (token) {
134                    case "/":
135                        continue;
136                    case "|":
137                        prefixSegs[i] = segs.size();
138                        continue;
139                    default:
140                        segs.add(token);
141                        continue;
142                    }
143                }
144                if (paths[i].endsWith("/") || paths[i].endsWith("|")) {
145                    segs.add("");
146                }
147                pathPatternElements[i] = segs.toArray(new String[0]);
148            }
149        }
150    }
151
152    /**
153     * @return the pattern (string) that was used to create this 
154     * resource pattern.
155     */
156    public String pattern() {
157        return pattern;
158    }
159
160    /**
161     * @return the protocol value specified in the pattern or {@code null}
162     */
163    public String protocol() {
164        return protocol;
165    }
166
167    /**
168     * @return the host value specified in the pattern or {@code null}
169     */
170    public String host() {
171        return host;
172    }
173
174    /**
175     * @return the port value specified in the pattern or {@code null}
176     */
177    public String port() {
178        return port;
179    }
180
181    /**
182     * @return the path value specified in the pattern or {@code null}
183     */
184    public String path() {
185        return path;
186    }
187
188    /**
189     * Matches the given resource URI against the pattern.
190     * 
191     * @param resource the URI specifying the resource to match
192     * @return -1 if the resource does not match, else the number
193     * of prefix segments (which may be 0)
194     */
195    @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.NPathComplexity",
196        "PMD.CollapsibleIfStatements", "PMD.DataflowAnomalyAnalysis",
197        "PMD.AvoidDuplicateLiterals" })
198    public int matches(URI resource) {
199        return matches(resource, Integer.MAX_VALUE);
200    }
201
202    /**
203     * Matches the given resource URI against the pattern, comparing
204     * at most the given number of path segments.
205     *
206     * @param resource the URI specifying the resource to match
207     * @param pathSegs the maximum number of path segments to compare
208     * @return -1 if the resource does not match, else the number
209     * of prefix segments (which may be 0)
210     */
211    @SuppressWarnings({ "PMD.CyclomaticComplexity", "PMD.NPathComplexity",
212        "PMD.CollapsibleIfStatements", "PMD.DataflowAnomalyAnalysis",
213        "PMD.CognitiveComplexity" })
214    public int matches(URI resource, int pathSegs) {
215        if (protocol != null && !"*".equals(protocol)) {
216            if (resource.getScheme() == null) {
217                return -1;
218            }
219            if (Arrays.stream(protocol.split(","))
220                .noneMatch(proto -> proto.equals(resource.getScheme()))) {
221                return -1;
222            }
223        }
224        if (host != null && !"*".equals(host)) {
225            if (resource.getHost() == null
226                || !resource.getHost().equals(host)) {
227                return -1;
228            }
229        }
230        if (port != null && !"*".equals(port)) {
231            if (Integer.parseInt(port) != resource.getPort()) {
232                return -1;
233            }
234        }
235
236        String[] reqElements = PathSpliterator.stream(resource.getPath())
237            .skip(1).toArray(size -> new String[size]);
238        String[] reqElementsPlus = null; // Created lazily
239        for (int pathIdx = 0; pathIdx < pathPatternElements.length; pathIdx++) {
240            String[] pathPattern = pathPatternElements[pathIdx];
241            if (prefixSegs[pathIdx] == pathPattern.length - 1
242                && lastIsEmpty(pathPattern)) {
243                // Special case, pattern ends with vertical bar
244                if (reqElementsPlus == null) {
245                    reqElementsPlus = reqElements;
246                    if (!lastIsEmpty(reqElementsPlus)) {
247                        reqElementsPlus = Arrays.copyOf(
248                            reqElementsPlus, reqElementsPlus.length + 1);
249                        reqElementsPlus[reqElementsPlus.length - 1] = "";
250                    }
251                }
252                if (matchPath(pathPattern, reqElementsPlus, pathSegs)) {
253                    return prefixSegs[pathIdx];
254                }
255            } else {
256                if (matchPath(pathPattern, reqElements, pathSegs)) {
257                    return prefixSegs[pathIdx];
258                }
259            }
260        }
261        return -1;
262    }
263
264    /**
265     * Matches the given pattern against the given resource URI.
266     * 
267     * @param pattern the pattern to match
268     * @param resource the URI specifying the resource to match
269     * @return {@code true} if the resource URI matches
270     * @throws ParseException if an invalid pattern is specified
271     */
272    public static boolean matches(String pattern, URI resource)
273            throws ParseException {
274        return new ResourcePattern(pattern).matches(resource) >= 0;
275    }
276
277    /**
278     * Matches the given pattern against the given resource URI, comparing
279     * at most the given number of path segments.
280     *
281     * @param pattern the pattern to match
282     * @param resource the URI specifying the resource to match
283     * @param pathSegments the maximum number of path segments to compare
284     * @return {@code true} if the resource URI matches
285     * @throws ParseException if an invalid pattern is specified
286     */
287    public static boolean matches(String pattern, URI resource,
288            int pathSegments)
289            throws ParseException {
290        return new ResourcePattern(pattern).matches(resource,
291            pathSegments) >= 0;
292    }
293
294    @SuppressWarnings("PMD.UseVarargs")
295    private static boolean lastIsEmpty(String[] elements) {
296        return elements.length > 0
297            && elements[elements.length - 1].length() == 0;
298    }
299
300    @SuppressWarnings({ "PMD.UseVarargs", "PMD.DataflowAnomalyAnalysis",
301        "PMD.PositionLiteralsFirstInComparisons",
302        "PMD.AvoidLiteralsInIfCondition" })
303    private boolean matchPath(String[] patternElements, String[] reqElements,
304            int maxSegs) {
305        int pathIdx = 0;
306        int reqIdx = 0;
307        int patternEls = Math.min(patternElements.length, maxSegs);
308        int reqEls = Math.min(reqElements.length, maxSegs);
309        while (true) {
310            if (pathIdx == patternEls) {
311                // Reached pattern end, is match if we also reached end
312                // of requested elements.
313                return reqIdx == reqEls;
314            }
315            if (reqIdx == reqEls) {
316                // Not pattern end, but more segments from request
317                // means no match.
318                return false;
319            }
320            String matchElement = patternElements[pathIdx++];
321            if ("**".equals(matchElement)) {
322                // Accept anything left means match.
323                return true;
324            }
325            String reqElement = reqElements[reqIdx++];
326            if (!"*".equals(matchElement) && !matchElement.equals(reqElement)) {
327                // If not equal (or wildcard) we have no match.
328                return false;
329            }
330        }
331
332    }
333
334    /**
335     * Removes the given number of segments (and their trailing slashes)
336     * from the beginning of the path. Segments may be empty. This implies 
337     * that invoking this method with a path that starts with a
338     * slash, the first removed segment is the empty segment
339     * preceding the slash and the starting slash. Put differently, 
340     * invoking this method with an absolute path and 1 makes the path
341     * relative.
342     *
343     * @param path the path
344     * @param segments the number of segments to remove
345     * @return the result
346     */
347    public static String removeSegments(String path, int segments) {
348        return PathSpliterator.stream(path)
349            .skip(segments).collect(Collectors.joining("/"));
350    }
351
352    /**
353     * Splits the given path in a prefix with the given number of
354     * segments and the rest. Like {{@link #removeSegments(String, int)}
355     * but additionally returning the removed segments.
356     *
357     * @param path the path
358     * @param segments the number of segments in the prefi
359     * @return the prefix and the rest
360     */
361    @SuppressWarnings({ "PMD.AssignmentInOperand",
362        "PMD.AvoidLiteralsInIfCondition" })
363    public static String[] split(String path, int segments) {
364        StringBuilder prefix = new StringBuilder();
365        StringBuilder suffix = new StringBuilder();
366        int[] count = { 0 };
367        PathSpliterator.stream(path).forEach(seg -> {
368            if (count[0]++ < segments) {
369                if (count[0] > 1) {
370                    prefix.append('/');
371                }
372                prefix.append(seg);
373            } else {
374                if (count[0] > segments + 1) {
375                    suffix.append('/');
376                }
377                suffix.append(seg);
378            }
379        });
380        return new String[] { prefix.toString(), suffix.toString() };
381    }
382
383    /**
384     * If the URI matches, returns the path split according to 
385     * the matched pattern (see {@link #split(String, int)}).
386     *
387     * @param resource the resource
388     * @return the result.
389     */
390    public Optional<String[]> splitPath(URI resource) {
391        int matchRes = matches(resource);
392        if (matchRes < 0) {
393            return Optional.empty();
394        }
395        return Optional.of(split(resource.getPath(), matchRes + 1));
396    }
397
398    /**
399     * If the URI matches, returns the path without prefix as specified
400     * by the matched pattern (see {@link #split(String, int)}).
401     *
402     * @param resource the resource
403     * @return the result.
404     */
405    @SuppressWarnings("PMD.AssignmentInOperand")
406    public Optional<String> pathRemainder(URI resource) {
407        int matchRes = matches(resource);
408        if (matchRes < 0) {
409            return Optional.empty();
410        }
411        int segments = matchRes + 1;
412        StringBuilder suffix = new StringBuilder();
413        int[] count = { 0 };
414        PathSpliterator.stream(resource.getPath()).forEach(seg -> {
415            if (count[0]++ >= segments) {
416                if (count[0] > segments + 1) {
417                    suffix.append('/');
418                }
419                suffix.append(seg);
420            }
421        });
422        return Optional.of(suffix.toString());
423    }
424
425    /*
426     * (non-Javadoc)
427     * 
428     * @see java.lang.Object#toString()
429     */
430    @Override
431    public String toString() {
432        StringBuilder builder = new StringBuilder(30);
433        builder.append("ResourcePattern [")
434            .append(pattern)
435            .append(']');
436        return builder.toString();
437    }
438
439    /*
440     * (non-Javadoc)
441     * 
442     * @see java.lang.Object#hashCode()
443     */
444    @Override
445    @SuppressWarnings("PMD.DataflowAnomalyAnalysis")
446    public int hashCode() {
447        @SuppressWarnings("PMD.AvoidFinalLocalVariable")
448        final int prime = 31;
449        int result = 1;
450        result = prime * result + ((pattern == null) ? 0 : pattern.hashCode());
451        return result;
452    }
453
454    /*
455     * (non-Javadoc)
456     * 
457     * @see java.lang.Object#equals(java.lang.Object)
458     */
459    @Override
460    public boolean equals(Object obj) {
461        if (this == obj) {
462            return true;
463        }
464        if (obj == null) {
465            return false;
466        }
467        if (getClass() != obj.getClass()) {
468            return false;
469        }
470        ResourcePattern other = (ResourcePattern) obj;
471        if (pattern == null) {
472            if (other.pattern != null) {
473                return false;
474            }
475        } else if (!pattern.equals(other.pattern)) {
476            return false;
477        }
478        return true;
479    }
480
481    /**
482     * Returns the segments of the path. If the path starts with a slash,
483     * an empty string is returned as first segment. If the path ends 
484     * with a slash, an empty string is returned as final segment.
485     */
486    public static class PathSpliterator extends AbstractSpliterator<String> {
487        private StringTokenizer tokenizer;
488        private boolean pendingLeadingEmpty;
489        private boolean pendingTrailingEmpty;
490
491        /**
492         * Creates a new stream for the given path, using "/"
493         * as path separator.
494         * 
495         * @param path the path
496         */
497        public static Stream<String> stream(String path) {
498            return StreamSupport.stream(new PathSpliterator(path), false);
499        }
500
501        /**
502         * Creates a new stream for the given path, using 
503         * the characters from delimiters as seperators.
504         * 
505         * @param path the path
506         * @param delimiters the delimiters
507         */
508        public static Stream<String> stream(String path, String delimiters) {
509            return StreamSupport.stream(
510                new PathSpliterator(path, delimiters), false);
511        }
512
513        /**
514         * Creates a new spliterator for the given path, using "/"
515         * as path separator.
516         * 
517         * @param path the path
518         */
519        public PathSpliterator(String path) {
520            this(path, "/");
521        }
522
523        /**
524         * Creates a new spliterator for the given path, using 
525         * the characters from delimiters as seperators.
526         * 
527         * @param path the path
528         * @param delimiters the delimiters
529         */
530        public PathSpliterator(String path, String delimiters) {
531            super(Long.MAX_VALUE, ORDERED | IMMUTABLE);
532            tokenizer = new StringTokenizer(path, delimiters);
533            pendingLeadingEmpty = path.startsWith("/");
534            pendingTrailingEmpty = path.endsWith("/");
535        }
536
537        /*
538         * (non-Javadoc)
539         * 
540         * @see java.util.Spliterator#tryAdvance(java.util.function.Consumer)
541         */
542        @Override
543        public boolean tryAdvance(Consumer<? super String> consumer) {
544            if (tokenizer == null) {
545                return false;
546            }
547            if (pendingLeadingEmpty) {
548                pendingLeadingEmpty = false;
549                consumer.accept("");
550                return true;
551            }
552            if (tokenizer.hasMoreTokens()) {
553                consumer.accept(tokenizer.nextToken());
554                return true;
555            }
556            tokenizer = null;
557            if (pendingTrailingEmpty) {
558                consumer.accept("");
559                return true;
560            }
561            return false;
562        }
563
564    }
565}