/** * Copyright (c) 2000-present Liferay, Inc. All rights reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 2.1 of the License, or (at your option) * any later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. */ package com.liferay.portal.kernel.util; import com.liferay.portal.kernel.concurrent.ConcurrentReferenceValueHashMap; import com.liferay.portal.kernel.memory.FinalizeManager; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * Parses strings into parameter maps and vice versa. * * @author Connor McKay * @author Brian Wing Shun Chan * @see com.liferay.portal.kernel.portlet.Route * @see Pattern */ public class StringParser { public static StringParser create(String chunk) { StringParser stringParser = _stringParserCache.get(chunk); if (stringParser == null) { stringParser = new StringParser(chunk); _stringParserCache.put(chunk, stringParser); } return stringParser; } /** * Escapes the special characters in the string so that they will have no * special meaning in a regular expression. * * <p> * This method differs from {@link Pattern#quote(String)} by escaping each * special character with a backslash, rather than enclosing the entire * string in special quote tags. This allows the escaped string to be * manipulated or have sections replaced with non-literal sequences. * </p> * * @param s the string to escape * @return the escaped string */ public static String escapeRegex(String s) { Matcher matcher = _escapeRegexPattern.matcher(s); return matcher.replaceAll("\\\\$0"); } /** * Builds a string from the parameter map if this parser is appropriate. * * <p> * A parser is appropriate if each parameter matches the format of its * accompanying fragment. * </p> * * <p> * If this parser is appropriate, all the parameters used in the pattern * will be removed from the parameter map. If this parser is not * appropriate, the parameter map will not be modified. * </p> * * @param parameters the parameter map to build the string from * @return the string, or <code>null</code> if this parser is not * appropriate */ public String build(Map<String, String> parameters) { Builder builder = null; for (StringParserFragment stringParserFragment : _stringParserFragments) { String value = parameters.get(stringParserFragment.getName()); if (value == null) { return null; } if ((_stringEncoder != null) && !stringParserFragment.isRaw()) { value = _stringEncoder.encode(value); } if (!stringParserFragment.matches(value)) { return null; } if (builder == null) { builder = _builderFactory.create(); } builder.setTokenValue(value); } for (StringParserFragment stringParserFragment : _stringParserFragments) { parameters.remove(stringParserFragment.getName()); } if (builder == null) { return _builderFactory._pattern; } return builder.toString(); } /** * Populates the parameter map with values parsed from the string if this * parser matches. * * @param s the string to parse * @param parameters the parameter map to populate if this parser matches * the string * @return <code>true</code> if this parser matches; <code>false</code> * otherwise */ public boolean parse(String s, Map<String, String> parameters) { Matcher matcher = _pattern.matcher(s); if (!matcher.matches()) { return false; } for (int i = 1; i <= _stringParserFragments.size(); i++) { StringParserFragment stringParserFragment = _stringParserFragments.get(i - 1); String value = matcher.group(i); if ((_stringEncoder != null) && !stringParserFragment.isRaw()) { value = _stringEncoder.decode(value); } parameters.put(stringParserFragment.getName(), value); } return true; } /** * Sets the string encoder to use for parsing or building a string. * * <p> * The string encoder will not be used for fragments marked as raw. A * fragment can be marked as raw by prefixing its name with a percent sign. * </p> * * @param stringEncoder the string encoder to use for parsing or building a * string * @see StringEncoder */ public void setStringEncoder(StringEncoder stringEncoder) { _stringEncoder = stringEncoder; } /** * Constructs a new string parser from the pattern. * * <p> * The pattern can be any string containing named fragments in brackets. The * following is a valid pattern for greeting: * </p> * * <p> * <pre> * <code> * Hi {name}! How are you? * </code> * </pre> * </p> * * <p> * This pattern would match the string "Hi Tom! How are you?". The * format of a fragment may optionally be specified by inserting a colon * followed by a regular expression after the fragment name. For instance, * <code>name</code> could be set to match only lower case letters with the * following: * </p> * * <p> * <pre> * <code> * Hi {name:[a-z]+}! How are you? * </code> * </pre> * </p> * * <p> * By default, a fragment will match anything except a forward slash or a * period. * </p> * * <p> * If a string parser is set to encode fragments using a {@link * StringEncoder}, an individual fragment can be specified as raw by * prefixing its name with a percent sign, as shown below: * </p> * * <p> * <pre> * <code> * /view_page/{%path:.*} * </code> * </pre> * </p> * * <p> * The format of the path fragment has also been specified to match anything * using the pattern ".*". This pattern could be used to parse the * string: * </p> * * <p> * <pre> * <code> * /view_page/root/home/mysite/pages/index.htm * </code> * </pre> * </p> * * <p> * <code>path</code> would be set to * "root/home/mysite/pages/index.htm", even if {@link * URLStringEncoder} had been set as the string encoder. * </p> * * <p> * <b>Do not include capturing subgroups in the pattern.</b> * </p> * * @param pattern the pattern string */ protected StringParser(String pattern) { String regex = escapeRegex(pattern); Matcher matcher = _fragmentPattern.matcher(pattern); _stringParserFragments = new ArrayList<>(matcher.groupCount()); int pos = 0; List<String> builderParts = new ArrayList<>(); String originalPattern = pattern; while (matcher.find()) { int start = matcher.start(); if (pos < start) { builderParts.add(originalPattern.substring(pos, start)); } pos = matcher.end(); builderParts.add(null); String chunk = matcher.group(); StringParserFragment stringParserFragment = StringParserFragment.create(chunk); _stringParserFragments.add(stringParserFragment); pattern = StringUtil.replace( pattern, chunk, stringParserFragment.getToken()); regex = StringUtil.replace( regex, escapeRegex(chunk), StringPool.OPEN_PARENTHESIS.concat( stringParserFragment.getPattern().concat( StringPool.CLOSE_PARENTHESIS))); } if (pos < originalPattern.length()) { builderParts.add(originalPattern.substring(pos)); } _builderFactory = new BuilderFactory(pattern, builderParts); _pattern = Pattern.compile(regex); } private static final Pattern _escapeRegexPattern = Pattern.compile( "[\\{\\}\\(\\)\\[\\]\\*\\+\\?\\$\\^\\.\\#\\\\]"); private static final Pattern _fragmentPattern = Pattern.compile( "\\{.+?\\}"); private static final Map<String, StringParser> _stringParserCache = new ConcurrentReferenceValueHashMap<>( FinalizeManager.SOFT_REFERENCE_FACTORY); private final BuilderFactory _builderFactory; private final Pattern _pattern; private StringEncoder _stringEncoder; private final List<StringParserFragment> _stringParserFragments; private static class Builder { public void setTokenValue(String value) { if (_parts[_index] == null) { _parts[_index++] = value; } else { _index++; _parts[_index++] = value; } } @Override public String toString() { StringBundler sb = new StringBundler(_parts); return sb.toString(); } private Builder(String[] parts) { _parts = parts; } private int _index; private final String[] _parts; } private static class BuilderFactory { public Builder create() { return new Builder(_parts.clone()); } private BuilderFactory(String pattern, List<String> parts) { _pattern = pattern; if (parts.isEmpty()) { _parts = null; } else { _parts = parts.toArray(new String[parts.size()]); } } private final String[] _parts; private final String _pattern; } }