/* * Copyright (c) 2014. Escalon System-Entwicklung, Dietrich Schulten * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for * the specific language governing permissions and limitations under the License. */ package de.escalon.hypermedia.affordance; import de.escalon.hypermedia.spring.AffordanceBuilder; import org.springframework.hateoas.TemplateVariable; import org.springframework.util.Assert; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * URI template with the ability to be partially expanded, no matter if its variables are required or not. Unsatisfied * variables are kept as variables. Other implementations either remove all unsatisfied variables or fail when required * variables are unsatisfied. This behavior is required due to the way an Affordance is created by {@link * AffordanceBuilder}, see package info for an overview of affordance creation. * * @author Dietrich Schulten * @see de.escalon.hypermedia.spring */ public class PartialUriTemplate { private static final Pattern VARIABLE_REGEX = Pattern.compile("\\{([\\?\\&#/]?)([\\w\\,\\.]+)(:??.*?)\\}"); private final List<String> urlComponents = new ArrayList<String>(); private final List<List<Integer>> variableIndices = new ArrayList<List<Integer>>(); private List<TemplateVariable> variables = new ArrayList<TemplateVariable>(); private List<String> variableNames = new ArrayList<String>(); /** * Creates a new {@link PartialUriTemplate} using the given template string. * * @param template * must not be {@literal null} or empty. */ public PartialUriTemplate(String template) { Assert.hasText(template, "Template must not be null or empty!"); Matcher matcher = VARIABLE_REGEX.matcher(template); // first group is the variable start without leading {: "", "/", "?", "#", // second group is the comma-separated name list without the trailing } of the variable int endOfPart = 0; while (matcher.find()) { // 0 is the current match, i.e. the entire variable expression int startOfPart = matcher.start(0); // add part before current match if (endOfPart < startOfPart) { final String partWithoutVariables = template.substring(endOfPart, startOfPart); final StringTokenizer stringTokenizer = new StringTokenizer(partWithoutVariables, "?", true); boolean inQuery = false; while (stringTokenizer.hasMoreTokens()) { final String token = stringTokenizer.nextToken(); if ("?".equals(token)) { inQuery = true; } else { if (!inQuery) { urlComponents.add(token); } else { urlComponents.add("?" + token); } variableIndices.add(Collections.<Integer>emptyList()); } } } endOfPart = matcher.end(0); // add current match as part final String variablePart = template.substring(startOfPart, endOfPart); urlComponents.add(variablePart); // collect variablesInPart and track for each part which variables it contains // group(1) is the variable head without the leading { TemplateVariable.VariableType type = TemplateVariable.VariableType.from(matcher.group(1)); // group(2) are the variable names String[] names = matcher.group(2) .split(","); List<Integer> variablesInPart = new ArrayList<Integer>(); for (String name : names) { TemplateVariable variable = new TemplateVariable(name, type); variablesInPart.add(variables.size()); variables.add(variable); variableNames.add(name); } variableIndices.add(variablesInPart); } // finish off remaining part if (endOfPart < template.length()) { urlComponents.add(template.substring(endOfPart)); variableIndices.add(Collections.<Integer>emptyList()); } } public List<String> getVariableNames() { return variableNames; } /** * Returns the template as uri components, without variable expansion. * * @return components of the Uri */ public PartialUriTemplateComponents asComponents() { return getUriTemplateComponents(Collections.<String, Object>emptyMap(), Collections.<String>emptyList()); } /** * Expands the template using given parameters * * @param parameters * for expansion in the order of appearance in the template, must not be empty * @return expanded template */ public PartialUriTemplateComponents expand(Object... parameters) { List<String> variableNames = getVariableNames(); Map<String, Object> parameterMap = new LinkedHashMap<String, Object>(); int i = 0; for (String variableName : variableNames) { if (i < parameters.length) { parameterMap.put(variableName, parameters[i++]); } else { break; } } return getUriTemplateComponents(parameterMap, Collections.<String>emptyList()); } /** * Expands the template using given parameters * * @param parameters * for expansion, must not be empty * @return expanded template */ public PartialUriTemplateComponents expand(Map<String, Object> parameters) { return getUriTemplateComponents(parameters, Collections.<String>emptyList()); } /** * Applies parameters to template variables. * * @param parameters * to apply to variables * @param requiredArgs * if not empty, retains given requiredArgs * @return uri components */ private PartialUriTemplateComponents getUriTemplateComponents(Map<String, Object> parameters, List<String> requiredArgs) { Assert.notNull(parameters, "Parameters must not be null!"); final StringBuilder baseUrl = new StringBuilder(urlComponents.get(0)); final StringBuilder queryHead = new StringBuilder(); final StringBuilder queryTail = new StringBuilder(); final StringBuilder fragmentIdentifier = new StringBuilder(); for (int i = 1; i < urlComponents.size(); i++) { final String part = urlComponents.get(i); final List<Integer> variablesInPart = variableIndices.get(i); if (variablesInPart.isEmpty()) { if (part.startsWith("?") || part.startsWith("&")) { queryHead.append(part); } else if (part.startsWith("#")) { fragmentIdentifier.append(part); } else { baseUrl.append(part); } } else { for (Integer variableInPart : variablesInPart) { final TemplateVariable variable = variables.get(variableInPart); final Object value = parameters.get(variable.getName()); if (value == null) { switch (variable.getType()) { case REQUEST_PARAM: case REQUEST_PARAM_CONTINUED: if (requiredArgs.isEmpty() || requiredArgs.contains(variable.getName())) { // query vars without value always go last (query tail) if (queryTail.length() > 0) { queryTail.append(','); } queryTail.append(variable.getName()); } break; case FRAGMENT: fragmentIdentifier.append(variable.toString()); break; case PATH_VARIABLE: if (queryHead.length() != 0) { // level 1 variable in query queryHead.append(variable.toString()); } else { baseUrl.append(variable.toString()); } break; case SEGMENT: baseUrl.append(variable.toString()); } } else { switch (variable.getType()) { case REQUEST_PARAM: case REQUEST_PARAM_CONTINUED: if (queryHead.length() == 0) { queryHead.append('?'); } else { queryHead.append('&'); } queryHead.append(variable.getName()) .append('=') .append(urlEncode(value.toString())); break; case SEGMENT: baseUrl.append('/'); // fall through case PATH_VARIABLE: if (queryHead.length() != 0) { // level 1 variable in query queryHead.append(urlEncode(value.toString())); } else { baseUrl.append(urlEncode(value.toString())); } break; case FRAGMENT: fragmentIdentifier.append('#'); fragmentIdentifier.append(urlEncode(value.toString())); break; } } } } } return new PartialUriTemplateComponents(baseUrl.toString(), queryHead.toString(), queryTail.toString(), fragmentIdentifier.toString(), variableNames); } private String urlEncode(String s) { try { return URLEncoder.encode(s, "UTF-8"); } catch (UnsupportedEncodingException e) { throw new IllegalStateException("failed to urlEncode " + s, e); } } /** * Strips all variables which are not required by any of the given action descriptors. If no action descriptors are * given, nothing will be stripped. * * @param actionDescriptors * to decide which variables are optional, may be empty * @return partial uri template components without optional variables, if actionDescriptors was not empty */ public PartialUriTemplateComponents stripOptionalVariables(List<ActionDescriptor> actionDescriptors) { return getUriTemplateComponents(Collections.<String, Object>emptyMap(), getRequiredArgNames(actionDescriptors)); } private List<String> getRequiredArgNames(List<ActionDescriptor> actionDescriptors) { List<String> ret = new ArrayList<String>(); for (ActionDescriptor actionDescriptor : actionDescriptors) { Map<String, ActionInputParameter> required = actionDescriptor.getRequiredParameters(); ret.addAll(required.keySet()); } return ret; } }