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 &quot;Hi Tom! How are you?&quot;. 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 &quot;.*&quot;. 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             * &quot;root/home/mysite/pages/index.htm&quot;, 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    }