001 /** 002 * Copyright (c) 2000-present Liferay, Inc. All rights reserved. 003 * 004 * This library is free software; you can redistribute it and/or modify it under 005 * the terms of the GNU Lesser General Public License as published by the Free 006 * Software Foundation; either version 2.1 of the License, or (at your option) 007 * any later version. 008 * 009 * This library is distributed in the hope that it will be useful, but WITHOUT 010 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 011 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 012 * details. 013 */ 014 015 package com.liferay.portal.kernel.util; 016 017 import com.liferay.portal.kernel.concurrent.ConcurrentReferenceValueHashMap; 018 import com.liferay.portal.kernel.memory.FinalizeManager; 019 020 import java.util.ArrayList; 021 import java.util.List; 022 import java.util.Map; 023 import java.util.regex.Matcher; 024 import java.util.regex.Pattern; 025 026 /** 027 * Parses strings into parameter maps and vice versa. 028 * 029 * @author Connor McKay 030 * @author Brian Wing Shun Chan 031 * @see com.liferay.portal.kernel.portlet.Route 032 * @see Pattern 033 */ 034 public class StringParser { 035 036 public static StringParser create(String chunk) { 037 StringParser stringParser = _stringParserFragmentsCache.get(chunk); 038 039 if (stringParser == null) { 040 stringParser = new StringParser(chunk); 041 042 _stringParserFragmentsCache.put(chunk, stringParser); 043 } 044 045 return stringParser; 046 } 047 048 /** 049 * Escapes the special characters in the string so that they will have no 050 * special meaning in a regular expression. 051 * 052 * <p> 053 * This method differs from {@link Pattern#quote(String)} by escaping each 054 * special character with a backslash, rather than enclosing the entire 055 * string in special quote tags. This allows the escaped string to be 056 * manipulated or have sections replaced with non-literal sequences. 057 * </p> 058 * 059 * @param s the string to escape 060 * @return the escaped string 061 */ 062 public static String escapeRegex(String s) { 063 Matcher matcher = _escapeRegexPattern.matcher(s); 064 065 return matcher.replaceAll("\\\\$0"); 066 } 067 068 /** 069 * Builds a string from the parameter map if this parser is appropriate. 070 * 071 * <p> 072 * A parser is appropriate if each parameter matches the format of its 073 * accompanying fragment. 074 * </p> 075 * 076 * <p> 077 * If this parser is appropriate, all the parameters used in the pattern 078 * will be removed from the parameter map. If this parser is not 079 * appropriate, the parameter map will not be modified. 080 * </p> 081 * 082 * @param parameters the parameter map to build the string from 083 * @return the string, or <code>null</code> if this parser is not 084 * appropriate 085 */ 086 public String build(Map<String, String> parameters) { 087 String s = _builder; 088 089 for (StringParserFragment stringParserFragment : 090 _stringParserFragments) { 091 092 String value = parameters.get(stringParserFragment.getName()); 093 094 if (value == null) { 095 return null; 096 } 097 098 if ((_stringEncoder != null) && !stringParserFragment.isRaw()) { 099 value = _stringEncoder.encode(value); 100 } 101 102 if (!stringParserFragment.matches(value)) { 103 return null; 104 } 105 106 s = StringUtil.replace(s, stringParserFragment.getToken(), value); 107 } 108 109 for (StringParserFragment stringParserFragment : 110 _stringParserFragments) { 111 112 parameters.remove(stringParserFragment.getName()); 113 } 114 115 return s; 116 } 117 118 /** 119 * Populates the parameter map with values parsed from the string if this 120 * parser matches. 121 * 122 * @param s the string to parse 123 * @param parameters the parameter map to populate if this parser matches 124 * the string 125 * @return <code>true</code> if this parser matches; <code>false</code> 126 * otherwise 127 */ 128 public boolean parse(String s, Map<String, String> parameters) { 129 Matcher matcher = _pattern.matcher(s); 130 131 if (!matcher.matches()) { 132 return false; 133 } 134 135 for (int i = 1; i <= _stringParserFragments.size(); i++) { 136 StringParserFragment stringParserFragment = 137 _stringParserFragments.get(i - 1); 138 139 String value = matcher.group(i); 140 141 if ((_stringEncoder != null) && !stringParserFragment.isRaw()) { 142 value = _stringEncoder.decode(value); 143 } 144 145 parameters.put(stringParserFragment.getName(), value); 146 } 147 148 return true; 149 } 150 151 /** 152 * Sets the string encoder to use for parsing or building a string. 153 * 154 * <p> 155 * The string encoder will not be used for fragments marked as raw. A 156 * fragment can be marked as raw by prefixing its name with a percent sign. 157 * </p> 158 * 159 * @param stringEncoder the string encoder to use for parsing or building a 160 * string 161 * @see StringEncoder 162 */ 163 public void setStringEncoder(StringEncoder stringEncoder) { 164 _stringEncoder = stringEncoder; 165 } 166 167 /** 168 * Constructs a new string parser from the pattern. 169 * 170 * <p> 171 * The pattern can be any string containing named fragments in brackets. The 172 * following is a valid pattern for greeting: 173 * </p> 174 * 175 * <p> 176 * <pre> 177 * <code> 178 * Hi {name}! How are you? 179 * </code> 180 * </pre> 181 * </p> 182 * 183 * <p> 184 * This pattern would match the string "Hi Tom! How are you?". The 185 * format of a fragment may optionally be specified by inserting a colon 186 * followed by a regular expression after the fragment name. For instance, 187 * <code>name</code> could be set to match only lower case letters with the 188 * following: 189 * </p> 190 * 191 * <p> 192 * <pre> 193 * <code> 194 * Hi {name:[a-z]+}! How are you? 195 * </code> 196 * </pre> 197 * </p> 198 * 199 * <p> 200 * By default, a fragment will match anything except a forward slash or a 201 * period. 202 * </p> 203 * 204 * <p> 205 * If a string parser is set to encode fragments using a {@link 206 * StringEncoder}, an individual fragment can be specified as raw by 207 * prefixing its name with a percent sign, as shown below: 208 * </p> 209 * 210 * <p> 211 * <pre> 212 * <code> 213 * /view_page/{%path:.*} 214 * </code> 215 * </pre> 216 * </p> 217 * 218 * <p> 219 * The format of the path fragment has also been specified to match anything 220 * using the pattern ".*". This pattern could be used to parse the 221 * string: 222 * </p> 223 * 224 * <p> 225 * <pre> 226 * <code> 227 * /view_page/root/home/mysite/pages/index.htm 228 * </code> 229 * </pre> 230 * </p> 231 * 232 * <p> 233 * <code>path</code> would be set to 234 * "root/home/mysite/pages/index.htm", even if {@link 235 * URLStringEncoder} had been set as the string encoder. 236 * </p> 237 * 238 * <p> 239 * <b>Do not include capturing subgroups in the pattern.</b> 240 * </p> 241 * 242 * @param pattern the pattern string 243 */ 244 protected StringParser(String pattern) { 245 String regex = escapeRegex(pattern); 246 247 Matcher matcher = _fragmentPattern.matcher(pattern); 248 249 _stringParserFragments = new ArrayList<>(matcher.groupCount()); 250 251 while (matcher.find()) { 252 String chunk = matcher.group(); 253 254 StringParserFragment stringParserFragment = 255 StringParserFragment.create(chunk); 256 257 _stringParserFragments.add(stringParserFragment); 258 259 pattern = StringUtil.replace( 260 pattern, chunk, stringParserFragment.getToken()); 261 262 regex = StringUtil.replace( 263 regex, escapeRegex(chunk), 264 StringPool.OPEN_PARENTHESIS.concat( 265 stringParserFragment.getPattern().concat( 266 StringPool.CLOSE_PARENTHESIS))); 267 } 268 269 _builder = pattern; 270 271 _pattern = Pattern.compile(regex); 272 } 273 274 private static final Pattern _escapeRegexPattern = Pattern.compile( 275 "[\\{\\}\\(\\)\\[\\]\\*\\+\\?\\$\\^\\.\\#\\\\]"); 276 private static final Pattern _fragmentPattern = Pattern.compile( 277 "\\{.+?\\}"); 278 private static final Map<String, StringParser> 279 _stringParserFragmentsCache = new ConcurrentReferenceValueHashMap<>( 280 FinalizeManager.SOFT_REFERENCE_FACTORY); 281 282 private final String _builder; 283 private final Pattern _pattern; 284 private StringEncoder _stringEncoder; 285 private final List<StringParserFragment> _stringParserFragments; 286 287 }