/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.internal; import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; import javaslang.Tuple; import javaslang.Tuple4; public class RoutePattern { private static final Pattern GLOB = Pattern /** ?| ** | * | :var | {var(:.*)} */ //.compile("\\?|/\\*\\*|\\*|\\:((?:[^/]+)+?) |\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); /** ? | **:name | * | :var | */ .compile("\\?|/\\*\\*(\\:(?:[^/]+))?|\\*|\\:((?:[^/]+)+?)|\\{((?:\\{[^/]+?\\}|[^/{}]|\\\\[{}])+?)\\}"); private static final Pattern SLASH = Pattern.compile("//+"); private final Function<String, RouteMatcher> matcher; private String pattern; private List<String> vars; private List<String> reverse; private boolean glob; public RoutePattern(final String verb, final String pattern) { requireNonNull(verb, "A HTTP verb is required."); requireNonNull(pattern, "A path pattern is required."); this.pattern = normalize(pattern); Tuple4<Function<String, RouteMatcher>, List<String>, List<String>, Boolean> result = rewrite(this, verb.toUpperCase(), this.pattern.replace("/**/", "/**")); matcher = result._1; vars = result._2; reverse = result._3; glob = result._4; } public boolean glob() { return glob; } public List<String> vars() { return vars; } public String pattern() { return pattern; } public String reverse(final Map<String, Object> vars) { return reverse.stream() .map(segment -> vars.getOrDefault(segment, segment).toString()) .collect(Collectors.joining("")); } public String reverse(final Object... value) { List<String> vars = vars(); Map<String, Object> hash = new HashMap<>(); for (int i = 0; i < Math.min(vars.size(), value.length); i++) { hash.put(vars.get(i), value[i]); } return reverse(hash); } public RouteMatcher matcher(final String path) { requireNonNull(path, "A path is required."); return matcher.apply(path); } private static Tuple4<Function<String, RouteMatcher>, List<String>, List<String>, Boolean> rewrite( final RoutePattern owner, final String verb, final String pattern) { List<String> vars = new LinkedList<>(); String rwrverb = verbs(verb); StringBuilder patternBuilder = new StringBuilder(rwrverb); Matcher matcher = GLOB.matcher(pattern); int end = 0; boolean regex = !rwrverb.equals(verb); List<String> reverse = new ArrayList<>(); boolean glob = false; while (matcher.find()) { String head = pattern.substring(end, matcher.start()); patternBuilder.append(Pattern.quote(head)); reverse.add(head); String match = matcher.group(); if ("?".equals(match)) { patternBuilder.append("([^/])"); reverse.add(match); regex = true; glob = true; } else if ("*".equals(match)) { patternBuilder.append("([^/]*)"); reverse.add(match); regex = true; glob = true; } else if (match.equals("/**")) { reverse.add(match); patternBuilder.append("($|/.*)"); regex = true; glob = true; } else if (match.startsWith("/**:")) { reverse.add(match.substring(1)); String varName = match.substring(4); patternBuilder.append("/(?<v").append(vars.size()).append(">($|.*))"); vars.add(varName); regex = true; glob = true; } else if (match.startsWith(":")) { regex = true; String varName = match.substring(1); patternBuilder.append("(?<v").append(vars.size()).append(">[^/]+)"); vars.add(varName); reverse.add(varName); } else if (match.startsWith("{") && match.endsWith("}")) { regex = true; int colonIdx = match.indexOf(':'); if (colonIdx == -1) { String varName = match.substring(1, match.length() - 1); patternBuilder.append("(?<v").append(vars.size()).append(">[^/]+)"); vars.add(varName); reverse.add(varName); } else { String varName = match.substring(1, colonIdx); String regexpr = match.substring(colonIdx + 1, match.length() - 1); patternBuilder.append("(?<v").append(vars.size()).append(">"); patternBuilder.append("**".equals(regexpr) ? "($|.*)" : regexpr); patternBuilder.append(')'); vars.add(varName); reverse.add(varName); } } end = matcher.end(); } String tail = pattern.substring(end, pattern.length()); reverse.add(tail); patternBuilder.append(Pattern.quote(tail)); return Tuple.of(fn(owner, regex, regex ? patternBuilder.toString() : verb + pattern, vars), vars, reverse, glob); } private static String verbs(final String verb) { String[] verbs = verb.split("\\|"); if (verbs.length == 1) { return verb.equals("*") ? "(?:[^/]*)" : verb; } return "(?:" + verb + ")"; } private static Function<String, RouteMatcher> fn(final RoutePattern owner, final boolean complex, final String pattern, final List<String> vars) { return new Function<String, RouteMatcher>() { final Pattern regex = complex ? Pattern.compile(pattern) : null; @Override public RouteMatcher apply(final String fullpath) { String path = fullpath.substring(fullpath.indexOf('/')); return complex ? new RegexRouteMatcher(path, regex.matcher(fullpath), vars) : new SimpleRouteMatcher(pattern, path, fullpath); } }; } public static String normalize(final String pattern) { if (pattern.equals("*")) { return "/**"; } if (pattern.equals("/")) { return "/"; } String normalized = SLASH.matcher(pattern).replaceAll("/"); if (normalized.equals("/")) { return "/"; } StringBuilder buffer = new StringBuilder(); if (!normalized.startsWith("/")) { buffer.append("/"); } buffer.append(normalized); if (normalized.endsWith("/")) { buffer.setLength(buffer.length() - 1); } return buffer.toString(); } @Override public String toString() { return pattern; } }