/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * Licensed 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.jboss.errai.ui.nav.client.local; import java.util.ArrayList; import java.util.List; import java.util.Map.Entry; import org.jboss.errai.ui.nav.client.local.api.PageNotFoundException; import com.google.common.collect.BiMap; import com.google.common.collect.HashBiMap; import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.ImmutableMultimap.Builder; import com.google.common.collect.Multimap; import com.google.gwt.regexp.shared.MatchResult; import com.google.gwt.regexp.shared.RegExp; /** * Used to match URLs typed in by the user to the correct {@link Page#path()} * * @author Max Barkley <mbarkley@redhat.com> * @author Divya Dadlani <ddadlani@redhat.com> * */ public class URLPatternMatcher { /** * Used to look up page names using a given URL pattern. */ private final BiMap<URLPattern, String> pageMap = HashBiMap.create(); private String defaultPageName; /** * Adds the allowed URL template as specified in the {@link Page#path()} by the developer. * * @param urlTemplate * The page URL pattern specified in the {@link Page#path()}. * @param pageName * The name of the page. */ public void add(String urlTemplate, String pageName) { final URLPattern urlPattern = generatePattern(urlTemplate); pageMap.put(urlPattern, pageName); } /** * Generates a {@link URLPattern} from a {@link Page#path()} * @param urlTemplate The {@link Page#path()} * @return A {@link URLPattern} used to match URLs */ public static URLPattern generatePattern(String urlTemplate) { final RegExp regex = RegExp.compile(URLPattern.paramRegex, "g"); final List<String> paramList = new ArrayList<String>(); MatchResult mr; final StringBuilder sb = new StringBuilder(); // Ensure matching at beginning of line sb.append("^"); // Match patterns with or without leading slash sb.append("/?"); int endOfPreviousPattern = 0; int startOfNextPattern = 0; while ((mr = regex.exec(urlTemplate)) != null) { addParamName(paramList, mr); startOfNextPattern = mr.getIndex(); // Append any string literal that may occur in the URL path // before the next parameter. sb.append(urlTemplate, endOfPreviousPattern, startOfNextPattern); // Append regex for matching the parameter value sb.append(URLPattern.urlSafe); endOfPreviousPattern = regex.getLastIndex(); } // Append any remaining trailing string literals sb.append(urlTemplate, endOfPreviousPattern, urlTemplate.length()); // Ensure matching at end of line sb.append("$"); return new URLPattern(RegExp.compile(sb.toString()), paramList, urlTemplate); } private static void addParamName(List<String> paramList, MatchResult mr) { paramList.add(mr.getGroup(1)); } /** * Creates a {@link HistoryToken} by parsing a URL path. This path should never include the application context. */ public HistoryToken parseURL(String url) { final Builder<String, String> mapBuilder = ImmutableMultimap.builder(); String keyValuePairs, pageInfo; final int indexOfSemicolon = url.indexOf(';'); if (indexOfSemicolon > 0) { pageInfo = url.substring(0, indexOfSemicolon); keyValuePairs = url.substring(indexOfSemicolon + 1); } else { pageInfo = url; keyValuePairs = null; } final String pageName = parseValues(pageInfo, mapBuilder); if (pageName == null) throw new PageNotFoundException("Invalid URL \"" + URLPattern.decodeParsingCharacters(url) + "\" could not be mapped to any page."); if (keyValuePairs != null) { parseKeyValuePairs(keyValuePairs, mapBuilder); } final Multimap<String, String> state = mapBuilder.build(); return new HistoryToken(pageName, ImmutableMultimap.copyOf(state), getURLPattern(pageName)); } private String parseValues(String rawURIPath, Builder<String, String> builder) { final String pageName = getPageName(rawURIPath); if (pageName == null) return null; final URLPattern pattern = getURLPattern(pageName); if (pattern.getParamList().size() == 0) return pageName; final MatchResult mr = pattern.getRegex().exec(rawURIPath); for (int keyIndex = 0; keyIndex < pattern.getParamList().size(); keyIndex++) { builder.put(URLPattern.decodeParsingCharacters(pattern.getParamList().get(keyIndex)), URLPattern .decodeParsingCharacters(mr.getGroup(keyIndex + 1))); } return pageName; } private void parseKeyValuePairs(String rawKeyValueString, Builder<String, String> builder) { StringBuilder key = new StringBuilder(); StringBuilder value = new StringBuilder(); // sb is a state cursor in this little parser: it always points to one of the // StringBuilders above; this is the one we're currently accumulating characters into. // you can also check the state of the parser by seeing which StringBuilder sb points at. StringBuilder sb = key; for (int i = 0, n = rawKeyValueString.length(); i < n; i++) { final char ch = rawKeyValueString.charAt(i); if (ch == '&') { builder.put(URLPattern.decodeParsingCharacters(key.toString()), URLPattern.decodeParsingCharacters(value.toString())); key = new StringBuilder(); value = new StringBuilder(); sb = key; } else if (ch == '=') { sb = value; } else { sb.append(ch); } } // we've got a key-value pair that still isn't in the map builder builder.put(URLPattern.decodeParsingCharacters(key.toString()), URLPattern.decodeParsingCharacters(value.toString())); } /** * Declares the default page to be matched against the empty string pattern. * @param defaultPage Never null. Must match a page that has already been added with {@link #add(String, String)} */ public void setAsDefaultPage(String defaultPage) { final URLPattern urlPattern = getURLPattern(defaultPage); if (urlPattern == null) throw new IllegalArgumentException("Page " + defaultPage + " must be added to URLPatternMatcher before it can be set as Default Page."); if (urlPattern.getParamList().size() > 0) throw new IllegalArgumentException("Cannot set a default page that has path parameters."); this.defaultPageName = defaultPage; } /** * @param pageName The name of the page corresponding to the {@link URLPattern} * @return The {@link URLPattern} for the given page name. */ public URLPattern getURLPattern(String pageName) { return pageMap.inverse().get(pageName); } /** * @return The name of the page to which the given user-entered URL corresponds. */ public String getPageName(String typedURL) { if (typedURL.equals("")) { return this.defaultPageName; } for (final Entry<URLPattern, String> urlMatcher : pageMap.entrySet()) { if (urlMatcher.getKey().matches(typedURL)) { return urlMatcher.getValue(); } } return null; } }