org.springframework.hateoas.UriTemplate Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of spring-hateoas Show documentation
Show all versions of spring-hateoas Show documentation
Library to support implementing representations for
hyper-text driven REST web services.
/*
* Copyright 2014-2024 the original author or authors.
*
* 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
*
* https://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 org.springframework.hateoas;
import java.io.Serializable;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.springframework.hateoas.TemplateVariable.VariableType;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
/**
* Custom URI template to support qualified URI template variables.
*
* @author Oliver Gierke
* @author JamesE Richardson
* @see https://tools.ietf.org/html/rfc6570
* @since 0.9
*/
public class UriTemplate implements Iterable, Serializable {
private static final Pattern VARIABLE_REGEX = Pattern
.compile("\\{([\\?\\/\\.\\+\\;]?)([\\w\\.(\\:\\d+)*%\\,*]+)\\}");
private static final Pattern ELEMENT_REGEX = Pattern.compile("([\\w\\.\\%]+)(\\:\\d+)?(\\*)?");
private static final long serialVersionUID = -1007874653930162262L;
private final TemplateVariables variables;
private final ExpandGroups groups;
private final String baseUri, template;
/**
* Creates a new {@link UriTemplate} using the given template string.
*
* @param template must not be {@literal null} or empty.
*/
private UriTemplate(String template) {
Assert.hasText(template, "Template must not be null or empty!");
int firstCurlyBraceIndex = template.indexOf('{');
template = prepareTemplate(template, firstCurlyBraceIndex);
String baseUri = template;
List variables = new ArrayList<>();
List expandGroups = new ArrayList<>();
if (firstCurlyBraceIndex != -1) {
Matcher matcher = VARIABLE_REGEX.matcher(template);
while (matcher.find()) {
String typeFlag = matcher.group(1);
String[] segments = matcher.group(2).split(",");
VariableType type = VariableType.from(typeFlag);
List variableGroup = new ArrayList<>();
for (String segment : segments) {
Matcher inner = ELEMENT_REGEX.matcher(segment);
while (inner.find()) {
String name = inner.group(1);
String limit = inner.group(2);
String composite = inner.group(3);
TemplateVariable variable = new TemplateVariable(name, type);
variable = StringUtils.hasText(composite) ? variable.composite() : variable;
variable = StringUtils.hasText(limit) ? variable.limit(Integer.valueOf(limit.substring(1))) : variable;
variableGroup.add(variable);
variables.add(variable);
}
}
expandGroups.add(new ExpandGroup(variableGroup));
}
}
this.variables = variables.isEmpty() ? TemplateVariables.NONE : new TemplateVariables(variables);
this.groups = new ExpandGroups(expandGroups);
this.baseUri = baseUri;
this.template = template;
}
/**
* Creates a new {@link UriTemplate} from the given base URI, {@link TemplateVariables} and {@link UriBuilderFactory}.
*
* @param baseUri must not be {@literal null} or empty.
* @param variables must not be {@literal null}.
*/
private UriTemplate(String baseUri, String template, TemplateVariables variables, ExpandGroups groups) {
Assert.hasText(baseUri, "Base URI must not be null or empty!");
Assert.notNull(variables, "Template variables must not be null!");
this.baseUri = baseUri;
this.variables = variables;
this.groups = groups;
this.template = template;
}
/**
* Returns a {@link UriTemplate} for the given {@link String} template.
*
* @param template must not be {@literal null} or empty.
* @return
*/
public static UriTemplate of(String template) {
Assert.hasText(template, "Template must not be null or empty!");
return new UriTemplate(template);
}
/**
* Returns a {@link UriTemplate} for the given {@link String} template.
*
* @param template must not be {@literal null} or empty.
* @return
*/
public static UriTemplate of(String template, TemplateVariables variables) {
Assert.hasText(template, "Template must not be null or empty!");
return new UriTemplate(template).with(variables);
}
/**
* Creates a new {@link UriTemplate} with the current {@link TemplateVariable}s augmented with the given ones.
*
* @param variables must not be {@literal null}.
* @return will never be {@literal null}.
*/
public UriTemplate with(TemplateVariables variables) {
Assert.notNull(variables, "TemplateVariables must not be null!");
if (variables.equals(TemplateVariables.NONE)) {
return this;
}
UriComponents components = UriComponentsBuilder.fromUriString(baseUri).build();
MultiValueMap parameters = components.getQueryParams();
List result = new ArrayList<>();
for (TemplateVariable variable : variables) {
boolean isRequestParam = variable.isRequestParameterVariable();
boolean alreadyPresent = parameters.containsKey(variable.getName());
if (isRequestParam && alreadyPresent) {
continue;
}
if (variable.isFragment() && StringUtils.hasText(components.getFragment())) {
continue;
}
// Use request parameter continuation if base contains parameters already
if (!parameters.isEmpty() && variable.getType().equals(VariableType.REQUEST_PARAM)) {
variable = variable.withType(VariableType.REQUEST_PARAM_CONTINUED);
}
result.add(variable);
}
String newOriginal = template;
ExpandGroups groups = this.groups;
MultiValueMap groupedByVariableType = new LinkedMultiValueMap<>();
for (TemplateVariable templateVariable : result) {
groupedByVariableType.add(templateVariable.getType(), templateVariable);
}
for (Entry> entry : groupedByVariableType.entrySet()) {
ExpandGroup existing = groups.findLastExpandGroupOfType(entry.getKey());
ExpandGroup group = new ExpandGroup(entry.getValue());
if (existing != null) {
group = existing.merge(group);
newOriginal = newOriginal.replace(existing.asString(), group.asString());
} else {
newOriginal = group.insertInto(newOriginal);
}
groups = groups.addOrAugment(group);
}
return new UriTemplate(baseUri, newOriginal, this.variables.concat(result), groups);
}
/**
* Creates a new {@link UriTemplate} with the given {@link TemplateVariable} added.
*
* @param variable must not be {@literal null}.
* @return will never be {@literal null}.
*/
public UriTemplate with(TemplateVariable variable) {
Assert.notNull(variable, "Template variable must not be null!");
return with(new TemplateVariables(variable));
}
/**
* Creates a new {@link UriTemplate} with a {@link TemplateVariable} with the given name and type added.
*
* @param variableName must not be {@literal null} or empty.
* @param type must not be {@literal null}.
* @return will never be {@literal null}.
*/
public UriTemplate with(String variableName, TemplateVariable.VariableType type) {
return with(new TemplateVariables(new TemplateVariable(variableName, type)));
}
/**
* Returns whether the given candidate is a URI template.
*
* @param candidate
* @return
*/
public static boolean isTemplate(String candidate) {
return StringUtils.hasText(candidate) //
? VARIABLE_REGEX.matcher(candidate).find()
: false;
}
/**
* Returns the {@link TemplateVariable}s discovered.
*
* @return
*/
public List getVariables() {
return variables.asList();
}
/**
* Returns the names of the variables discovered.
*
* @return
*/
public List getVariableNames() {
return variables.asList().stream() //
.map(TemplateVariable::getName) //
.collect(Collectors.toList());
}
/**
* Expands the {@link UriTemplate} using the given parameters. The values will be applied in the order of the
* variables discovered.
*
* @param parameters
* @return
* @see #expand(Map)
*/
public URI expand(Object... parameters) {
if (TemplateVariables.NONE.equals(variables)) {
return URI.create(baseUri);
}
Iterator