/******************************************************************************* * Copyright (c) 2008 - 2014 Red Hat, Inc. and others. * Distributed under license by Red Hat, Inc. All rights reserved. * This program is made available under the terms of the * Eclipse Public License v1.0 which accompanies this distribution, * and is available at http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Xavier Coulon - Initial API and implementation ******************************************************************************/ package org.jboss.tools.ws.ui.utils; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.jboss.tools.ws.ui.dialogs.EnumParamType; import org.jboss.tools.ws.ui.dialogs.URLTemplateParameter; /** * Utility class to parse an URL Template and extract all the parameters from * it. * * @author xcoulon * */ public class JAXRSPathTemplateParser { /** Pattern to match expression such as {@code "{name:int}" }.*/ private static final Pattern PATH_PARAM_TEMPLATE_PATTERN = Pattern.compile("\\{(\\w+):([^:]+)\\}"); //$NON-NLS-1$ /** Pattern to match expression such as {@code "name={int}" }.*/ private static final Pattern OPTIONAL_PARAM_TEMPLATE_PATTERN = Pattern.compile("(;|\\?|&)(\\w+)=\\{([^:]+)\\}"); //$NON-NLS-1$ /** Pattern to match expression such as <pre>name={String:"FOO"}</pre> or <pre>name={int:"123.456"}</pre>. */ private static final Pattern OPTIONAL_PARAM_WITH_DEFAULT_VALUE_TEMPLATE_PATTERN = Pattern.compile("(;|\\?|&)(\\w+)=\\{([^:]+):\"(.+)\"\\}"); //$NON-NLS-1$ /** * Parses the given urlTemplate and extracts all * {@link URLTemplateParameter}s from it. * <pathParamPattern> * Supports a mix of syntaxes: * <ul> * <li>{@code /rest/members/id:int}</li> * <li>{@code /rest/members/id:[0-9][0-9]*}</li> * <li>{@code /rest/members;matrix=int}?start={int}&size={int}}</li> * * and supports also syntax with {@code List} and {@code Set}, such as: * {@code /rest/members/query?from=int&to={int}&orderBy={orderBy:List<String>}} * * </ul> * * <p>Finally, it should also work with the following kind of syntax:</p> * {@code rest/members/user/{id:int}/{encoding:(/encoding/[^/]+?)?};matrix={String}?start={int} * * @param urlTemplate * @return a list of {@link URLTemplateParameter}s, or emty list if none was * found. * */ public static URLTemplateParameter[] parse(final String urlTemplate) { // quick fail if (urlTemplate == null || urlTemplate.isEmpty()) { return new URLTemplateParameter[0]; } final List<URLTemplateParameter> templateParameters = new ArrayList<URLTemplateParameter>(); // let's scan the given template, looking for content between '{' and // '}' characters int scanIndex = 0; while (scanIndex != -1 || scanIndex >= urlTemplate.length()) { final int bracketBeginIndex = urlTemplate.indexOf('{', scanIndex); final int bracketEndIndex = urlTemplate.indexOf('}', bracketBeginIndex); final int commaBeginIndex = urlTemplate.indexOf(';', scanIndex); final int questionMarkBeginIndex = urlTemplate.indexOf('?', scanIndex); final int ampersandMarkBeginIndex = urlTemplate.indexOf('&', scanIndex); if (bracketBeginIndex == -1 || bracketEndIndex == -1) { break; } // now, let see which character comes first: bracket, comma, question mark or ampersand int nextCharacterIndex = nextCharacterIndex(bracketBeginIndex, commaBeginIndex, questionMarkBeginIndex, ampersandMarkBeginIndex); if(nextCharacterIndex == -1) { break; } final char nextCharacter = urlTemplate.charAt(nextCharacterIndex); if(nextCharacter == '{') { final String expression = urlTemplate.substring(nextCharacterIndex, bracketEndIndex + 1); final Matcher pathParamTemplateMatcher = PATH_PARAM_TEMPLATE_PATTERN.matcher(expression); if(pathParamTemplateMatcher.matches()) { final URLTemplateParameter templateParameter = URLTemplateParameter.Builder.from(expression) .withName(pathParamTemplateMatcher.group(1), true) .withDatatype(pathParamTemplateMatcher.group(2)).withParamType(EnumParamType.PATH_PARAM) .build(); templateParameters.add(templateParameter); } scanIndex = nextCharacterIndex+expression.length(); } else if(nextCharacter == ';' || nextCharacter == '?' || nextCharacter == '&') { final String expression = urlTemplate.substring(nextCharacterIndex, bracketEndIndex + 1); final Matcher optionalParamTemplateMatcher = OPTIONAL_PARAM_TEMPLATE_PATTERN.matcher(expression); final Matcher optionalParamWithDefaultStringValueTemplateMatcher = OPTIONAL_PARAM_WITH_DEFAULT_VALUE_TEMPLATE_PATTERN.matcher(expression); if(optionalParamTemplateMatcher.matches()) { final URLTemplateParameter templateParameter = URLTemplateParameter.Builder.from(expression) .withName(optionalParamTemplateMatcher.group(2), true) .withDatatype(optionalParamTemplateMatcher.group(3)) .withParamType(getParamType(nextCharacter)).build(); templateParameters.add(templateParameter); } else if(optionalParamWithDefaultStringValueTemplateMatcher.matches()) { final URLTemplateParameter templateParameter = URLTemplateParameter.Builder.from(expression) .withName(optionalParamWithDefaultStringValueTemplateMatcher.group(2), true) .withDatatype(optionalParamWithDefaultStringValueTemplateMatcher.group(3)) .withDefaultValue(optionalParamWithDefaultStringValueTemplateMatcher.group(4)) .withParamType(getParamType(nextCharacter)).build(); templateParameters.add(templateParameter); } scanIndex = nextCharacterIndex+expression.length(); } } return templateParameters.toArray(new URLTemplateParameter[templateParameters.size()]); } /** * @param nextCharacter * @return the {@link EnumParamType} given the character: * <ul> * <li>{@code ;} -> {@link EnumParamType#MATRIX_PARAM}</li> * <li>{@code ?} -> {@link EnumParamType#QUERY_PARAM}</li> * <li>{@code &} -> {@link EnumParamType#QUERY_PARAM}</li> *</ul> */ private static EnumParamType getParamType(final char nextCharacter) { switch(nextCharacter) { case '?': case '&': return EnumParamType.QUERY_PARAM; case ';': return EnumParamType.MATRIX_PARAM; } return null; } /** * Returns the lowest character index value in the given values, excluding the {@code -1} value. * @param characterIndexes * @return the lowest characterIndex value */ private static int nextCharacterIndex(final int... characterIndexes) { int minValue = Integer.MAX_VALUE; for(int characterIndex : characterIndexes) { if(characterIndex != -1) { minValue = Math.min(minValue, characterIndex); } } if(minValue ==Integer.MAX_VALUE) { return -1; } return minValue; } /** * Utility class to search for the last position of a token amongst a set of * given tokens in a {@link String} * * @author xcoulon * */ static class CharSearcher { private final char[] tokens; private CharSearcher(char[] tokens) { this.tokens = tokens; } static CharSearcher findLastIndexOf(final char... tokens) { return new CharSearcher(tokens); } /** * Returns the highest index of the tokens locations in the given * content * * @param content * @return */ public int in(final String content) { int location = -1; for(char token : tokens) { location = Math.max(location, content.lastIndexOf(token)); } return location; } } }