package org.etk.core.rest.impl.uri; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; public class UriTemplateParser { /** * Pattern for process URI parameters, for example /a/b/{x}/c . */ public static final Pattern URI_PARAMETERS_PATTERN = Pattern.compile("\\{[^\\}^\\{]+\\}"); /** * Regex character, this characters in URI template will be escaped by * addition '\'. */ private static final String REGEX_CHARACTERS = ".?()"; /** * Should be added in regular expression instead URI parameter. */ private static final String URI_PARAMETER_TEMPLATE = "[^/]+?"; /** * URI parameter names. */ private List<String> names = new ArrayList<String>(); /** * Regular expression buffer. */ private StringBuffer regex = new StringBuffer(); /** * Normalized template, whitespace must be removed. */ private StringBuffer normalizedTemplate = new StringBuffer(); /** * Number of explicit characters in URI template, all characters except * parameters. */ private int numberOfCharacters = 0; /** * Actual processed parameter name. */ private String name; /** * Indexes regular expression capturing group. */ private List<Integer> groupIndexes = new ArrayList<Integer>(); /** * @param template source URI template */ public UriTemplateParser(String template) { Matcher m = URI_PARAMETERS_PATTERN.matcher(template); int start = 0; while (m.find()) { numberOfCharacters += addCharacter(template, start, m.start()); parseRegex(template.substring(m.start() + 1, m.end() - 1)); start = m.end(); } numberOfCharacters += addCharacter(template, start, template.length()); } /** * Get the number of literal characters in the template. * * @return number of literal characters in the template */ public final int getNumberOfLiteralCharacters() { return numberOfCharacters; } /** * @return list of names */ public final List<String> getParameterNames() { return names; } /** * Get the regular expression. * * @return the regular expression */ public final String getRegex() { return regex.toString(); } /** * Get the URI template. * * @return the URI template */ public final String getTemplate() { return normalizedTemplate.toString(); } /** * @return indexes of regular expression capturing group */ public final int[] getGroupIndexes() { if (names.isEmpty()) return null; int[] indxs = new int[names.size() + 1]; indxs[0] = 1; for (int i = 1; i < indxs.length; i++) indxs[i] = indxs[i - 1] + groupIndexes.get(i - 1); // Check are groups indexes goes one by one. for (int i = 0; i < indxs.length; i++) { if (indxs[i] != i + 1) return indxs; } return null; } /** * Encode set of literal characters. Characters must not e double encoded. * * @param literalCharacters source string * @return encoded string */ protected String encodeLiteralCharacters(String literalCharacters) { return UriComponent.recognizeEncode(literalCharacters, UriComponent.PATH, false); } /** * @param str part of source template between '{' and '}' */ private void parseRegex(String str) { int length = str.length(); int p = parseName(str, 0, length); String reg = ""; if (p == length) { reg = URI_PARAMETER_TEMPLATE; addToTemplate(name); } else { reg = str.substring(p + 1).trim(); if (reg.length() == 0) { reg = URI_PARAMETER_TEMPLATE; addToTemplate(name); } else { addToTemplate(name, reg); } } // Count how many groups has current part of template Pattern gp = Pattern.compile(reg); Matcher m = gp.matcher(""); groupIndexes.add(m.groupCount() + 1); regex.append('(').append(reg).append(')'); } /** * Add name of parameter to normalized template. * * @param name parameter name */ private void addToTemplate(String name) { normalizedTemplate.append('{').append(name).append('}'); } /** * Add name of parameter and regular expression corresponded to name to * normalized template. * * @param name parameter name * @param reg regular expression */ private void addToTemplate(String name, String reg) { normalizedTemplate.append('{').append(name).append('}').append(':').append(reg); } /** * @param str Part of source URI template between '{' and '}'. * @param p start position * @param length string length * @return URI template parameter name */ private int parseName(String str, int p, int length) { StringBuffer sb = new StringBuffer(); for (; p < length; p++) { char ch = str.charAt(p); if (ch == ' ') continue; // skip whitespace else if (Character.isLetterOrDigit(ch) || ch == '-' || ch == '_' || ch == '.') sb.append(ch); else if (ch == ':') break; else throw new IllegalArgumentException("Wrong character at part " + str); } name = sb.toString(); // TODO remove restriction that not allowed have few path parameters with // the same name. This should be allowed but part of URI template also MUST // be the same. E.g. /a/{x}/b/{x} and /a/{x:\d+}/b/{x:\d+} must be allowed, // but /a/{x}/b/{x:\d+} is not allowed. This task is not high priority. if (names.contains(name)) throw new IllegalArgumentException("URI template variables name " + name + " already registered."); names.add(name); return p; } /** * Add explicit characters to normalized URI template. * * @param template source URI template * @param start start position for reading characters * @param end end position for reading characters * @return how many characters was red */ private int addCharacter(String template, int start, int end) { String str = encodeLiteralCharacters(template.substring(start, end)); for (int i = 0; i < str.length(); i++) { char ch = str.charAt(i); // check is character corresponds to regular expression character if (REGEX_CHARACTERS.indexOf(ch) != -1) regex.append('\\'); regex.append(ch); normalizedTemplate.append(ch); } return end - start; } }