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