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