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    }