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}