All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.springframework.hateoas.Link Maven / Gradle / Ivy

/*
 * Copyright 2012-2022 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.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

/**
 * Value object for links.
 *
 * @author Oliver Gierke
 * @author Greg Turnquist
 * @author Jens Schauder
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(value = { "templated", "template" }, ignoreUnknown = true)
public class Link implements Serializable {

	private static final long serialVersionUID = -9037755944661782121L;
	private static final Pattern URI_AND_ATTRIBUTES_PATTERN = Pattern.compile("<(.*)>;(.*)");

	public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";

	private LinkRelation rel;
	private String href;
	private @Nullable String hreflang, media, title, type, deprecation, profile, name;
	private @JsonIgnore @Nullable UriTemplate template;
	private @JsonIgnore List affordances;

	/**
	 * Creates a new {@link Link} to the given URI with the given rel.
	 *
	 * @param href must not be {@literal null} or empty.
	 * @param rel must not be {@literal null} or empty.
	 */
	protected Link(String href, LinkRelation rel) {
		this(href, templateOrNull(href), rel, Collections.emptyList());
	}

	/**
	 * Creates a new Link from the given {@link UriTemplate} and rel.
	 *
	 * @param template must not be {@literal null}.
	 * @param rel must not be {@literal null} or empty.
	 */
	protected Link(UriTemplate template, LinkRelation rel) {
		this(template, rel, Collections.emptyList());
	}

	/**
	 * Creates a new Link from the given {@link UriTemplate}, link relation and affordances.
	 *
	 * @param template must not be {@literal null}.
	 * @param rel must not be {@literal null} or empty.
	 */
	private Link(UriTemplate template, LinkRelation rel, List affordances) {

		Assert.notNull(template, "UriTemplate must not be null!");
		Assert.notNull(rel, "LinkRelation must not be null!");
		Assert.notNull(affordances, "Affordances must not be null!");

		this.template = template;
		this.rel = rel;
		this.href = template.toString();
		this.affordances = affordances;
	}

	private Link(String href, @Nullable UriTemplate template, LinkRelation rel, List affordances) {

		Assert.hasText(href, "Href must not be null or empty!");
		Assert.notNull(rel, "LinkRelation must not be null!");
		Assert.notNull(affordances, "Affordances must not be null!");

		this.href = href;
		this.template = template;
		this.rel = rel;
		this.affordances = affordances;
	}

	private Link(LinkRelation rel, String href, @Nullable String hreflang, @Nullable String media, @Nullable String title,
			@Nullable String type, @Nullable String deprecation, @Nullable String profile, @Nullable String name,
			@Nullable UriTemplate template, List affordances) {

		this.rel = rel;
		this.href = href;
		this.hreflang = hreflang;
		this.media = media;
		this.title = title;
		this.type = type;
		this.deprecation = deprecation;
		this.profile = profile;
		this.name = name;
		this.template = template;
		this.affordances = affordances;
	}

	/**
	 * Creates a new link to the given URI with the self relation.
	 *
	 * @see IanaLinkRelations#SELF
	 * @param href must not be {@literal null} or empty.
	 * @return
	 * @since 1.1
	 */
	public static Link of(String href) {
		return new Link(href, IanaLinkRelations.SELF);
	}

	/**
	 * Creates a new {@link Link} to the given href with the given relation.
	 *
	 * @param href must not be {@literal null} or empty.
	 * @param relation must not be {@literal null} or empty.
	 * @return
	 * @since 1.1
	 */
	public static Link of(String href, String relation) {
		return new Link(href, LinkRelation.of(relation));
	}

	/**
	 * Creates a new {@link Link} to the given href and {@link LinkRelation}.
	 *
	 * @param href must not be {@literal null} or empty.
	 * @param relation must not be {@literal null}.
	 * @return
	 * @since 1.1
	 */
	public static Link of(String href, LinkRelation relation) {
		return new Link(href, relation);
	}

	/**
	 * Creates a new {@link Link} to the given {@link UriTemplate} and link relation.
	 *
	 * @param template must not be {@literal null}.
	 * @param relation must not be {@literal null} or empty.
	 * @return
	 * @since 1.1
	 */
	public static Link of(UriTemplate template, String relation) {
		return new Link(template, LinkRelation.of(relation));
	}

	/**
	 * Creates a new {@link Link} to the given {@link UriTemplate} and {@link LinkRelation}.
	 *
	 * @param template must not be {@literal null}.
	 * @param relation must not be {@literal null}.
	 * @return
	 * @since 1.1
	 */
	public static Link of(UriTemplate template, LinkRelation relation) {
		return new Link(template, relation);
	}

	/**
	 * Empty constructor required by the marshaling framework.
	 */
	protected Link() {
		this.affordances = new ArrayList<>();
	}

	/**
	 * Returns safe copy of {@link Affordance}s.
	 *
	 * @return
	 */
	public List getAffordances() {
		return Collections.unmodifiableList(this.affordances);
	}

	/**
	 * Returns a {@link Link} pointing to the same URI but with the {@code self} relation.
	 *
	 * @return
	 */
	public Link withSelfRel() {
		return withRel(IanaLinkRelations.SELF);
	}

	/**
	 * Create new {@link Link} with an additional {@link Affordance}.
	 *
	 * @param affordance must not be {@literal null}.
	 * @return
	 */
	public Link andAffordance(Affordance affordance) {

		Assert.notNull(affordance, "Affordance must not be null!");

		List newAffordances = new ArrayList<>(this.affordances);
		newAffordances.add(affordance);

		return withAffordances(newAffordances);
	}

	/**
	 * Create new {@link Link} with additional {@link Affordance}s.
	 *
	 * @param affordances must not be {@literal null}.
	 * @return
	 */
	public Link andAffordances(List affordances) {

		List newAffordances = new ArrayList<>();
		newAffordances.addAll(this.affordances);
		newAffordances.addAll(affordances);

		return withAffordances(newAffordances);
	}

	/**
	 * Creats a new {@link Link} with the given {@link Affordance}s.
	 *
	 * @param affordances must not be {@literal null}.
	 * @return
	 */
	public Link withAffordances(List affordances) {

		return new Link(this.rel, this.href, this.hreflang, this.media, this.title, this.type, this.deprecation,
				this.profile, this.name, this.template, affordances);
	}

	/**
	 * Returns the variable names contained in the template.
	 *
	 * @return
	 */
	@JsonIgnore
	public List getVariableNames() {

		UriTemplate template = this.template;

		return template == null ? Collections.emptyList() : template.getVariableNames();
	}

	/**
	 * Returns all {@link TemplateVariables} contained in the {@link Link}.
	 *
	 * @return
	 */
	@JsonIgnore
	public List getVariables() {

		UriTemplate template = this.template;

		return template == null ? Collections.emptyList() : template.getVariables();
	}

	/**
	 * Returns whether or not the link is templated.
	 *
	 * @return
	 */
	public boolean isTemplated() {

		UriTemplate template = this.template;

		return template == null ? false : !template.getVariables().isEmpty();
	}

	/**
	 * Turns the current template into a {@link Link} by expanding it using the given parameters.
	 *
	 * @param arguments
	 * @return
	 */
	@SuppressWarnings("null")
	public Link expand(Object... arguments) {

		UriTemplate template = this.template;

		return template == null ? this : of(template.expand(arguments).toString(), getRel());
	}

	/**
	 * Turns the current template into a {@link Link} by expanding it using the given parameters.
	 *
	 * @param arguments must not be {@literal null}.
	 * @return
	 */
	public Link expand(Map arguments) {

		UriTemplate template = this.template;

		return template == null ? this : of(template.expand(arguments).toString(), getRel());
	}

	/**
	 * Creates a new {@link Link} with the same href but given {@link LinkRelation}.
	 *
	 * @param relation must not be {@literal null}.
	 * @return
	 */
	public Link withRel(LinkRelation relation) {

		Assert.notNull(relation, "LinkRelation must not be null!");

		return new Link(relation, href, hreflang, media, title, type, deprecation, profile, name, template, affordances);
	}

	/**
	 * Creates a new {@link Link} with the same href but given {@link LinkRelation}.
	 *
	 * @param relation must not be {@literal null} or empty.
	 * @return
	 */
	public Link withRel(String relation) {
		return withRel(LinkRelation.of(relation));
	}

	/**
	 * Returns whether the current {@link Link} has the given link relation.
	 *
	 * @param rel must not be {@literal null} or empty.
	 * @return
	 */
	public boolean hasRel(String rel) {

		Assert.hasText(rel, "Link relation must not be null or empty!");

		return hasRel(LinkRelation.of(rel));
	}

	/**
	 * Returns whether the {@link Link} has the given {@link LinkRelation}.
	 *
	 * @param rel must not be {@literal null}.
	 * @return
	 */
	public boolean hasRel(LinkRelation rel) {

		Assert.notNull(rel, "Link relation must not be null!");

		return this.rel.isSameAs(rel);
	}

	/**
	 * Returns the current href as URI after expanding the links without any arguments, i.e. all optional URI
	 * {@link TemplateVariable}s will be dropped. If the href contains mandatory {@link TemplateVariable}s, the URI
	 * creation will fail with an {@link IllegalStateException}.
	 *
	 * @return will never be {@literal null}.
	 * @throws IllegalStateException in case the href contains mandatory URI {@link TemplateVariable}s.
	 */
	public URI toUri() {

		try {
			return URI.create(expand().getHref());
		} catch (IllegalArgumentException o_O) {
			throw new IllegalStateException(o_O);
		}
	}

	/**
	 * Factory method to easily create {@link Link} instances from RFC-8288 compatible {@link String} representations of a
	 * link.
	 *
	 * @param element an RFC-8288 compatible representation of a link.
	 * @throws IllegalArgumentException if a {@link String} was given that does not adhere to RFC-8288.
	 * @throws IllegalArgumentException if no {@code rel} attribute could be found.
	 * @return
	 */
	public static Link valueOf(String element) {

		if (!StringUtils.hasText(element)) {
			throw new IllegalArgumentException(String.format("Given link header %s is not RFC-8288 compliant!", element));
		}

		Matcher matcher = URI_AND_ATTRIBUTES_PATTERN.matcher(element);

		if (matcher.find()) {

			Map attributes = getAttributeMap(matcher.group(2));

			if (!attributes.containsKey("rel")) {
				throw new IllegalArgumentException("Link does not provide a rel attribute!");
			}

			LinkRelation rel = LinkRelation.of(attributes.get("rel"));
			String href = matcher.group(1);
			String hrefLang = attributes.get("hreflang");
			String media = attributes.get("media");
			String title = attributes.get("title");
			String type = attributes.get("type");
			String deprecation = attributes.get("deprecation");
			String profile = attributes.get("profile");
			String name = attributes.get("name");

			return new Link(rel, href, hrefLang, media, title, type, deprecation, profile, name, templateOrNull(href),
					Collections.emptyList());

		} else {
			throw new IllegalArgumentException(String.format("Given link header %s is not RFC-8288 compliant!", element));
		}
	}

	/**
	 * Parses the links attributes from the given source {@link String}.
	 *
	 * @param source
	 * @return
	 */
	private static Map getAttributeMap(String source) {

		if (!StringUtils.hasText(source)) {
			return Collections.emptyMap();
		}

		String[] parts = source.split(";");
		Map attributes = new HashMap<>();

		for (String part : parts) {

			int delimiter = part.indexOf('=');

			String key = part.substring(0, delimiter).trim();
			String value = part.substring(delimiter + 1).trim();

			// Potentially unquote value
			value = value.startsWith("\"") ? value.substring(1, value.length() - 1) : value;

			attributes.put(key, value);
		}

		return attributes;
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal href}.
	 *
	 * @param href
	 * @return
	 */
	public Link withHref(String href) {

		return this.href == href ? this
				: new Link(this.rel, href, this.hreflang, this.media, this.title, this.type, this.deprecation, this.profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal hrefleng}.
	 *
	 * @param hreflang
	 * @return
	 */
	public Link withHreflang(String hreflang) {

		return this.hreflang == hreflang ? this
				: new Link(this.rel, this.href, hreflang, this.media, this.title, this.type, this.deprecation, this.profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal media}.
	 *
	 * @param media
	 * @return
	 */
	public Link withMedia(String media) {

		return this.media == media ? this
				: new Link(this.rel, this.href, this.hreflang, media, this.title, this.type, this.deprecation, this.profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal title}.
	 *
	 * @param title
	 * @return
	 */
	public Link withTitle(String title) {

		return this.title == title ? this
				: new Link(this.rel, this.href, this.hreflang, this.media, title, this.type, this.deprecation, this.profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal type}.
	 *
	 * @param type
	 * @return
	 */
	public Link withType(String type) {

		return this.type == type ? this
				: new Link(this.rel, this.href, this.hreflang, this.media, this.title, type, this.deprecation, this.profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal deprecation}.
	 *
	 * @param deprecation
	 * @return
	 */
	public Link withDeprecation(String deprecation) {

		return this.deprecation == deprecation ? this
				: new Link(this.rel, this.href, this.hreflang, this.media, this.title, this.type, deprecation, this.profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal profile}.
	 *
	 * @param profile
	 * @return
	 */
	public Link withProfile(String profile) {

		return this.profile == profile ? this
				: new Link(this.rel, this.href, this.hreflang, this.media, this.title, this.type, this.deprecation, profile,
						this.name, this.template, this.affordances);
	}

	/**
	 * Create a new {@link Link} by copying all attributes and applying the new {@literal name}.
	 *
	 * @param name
	 * @return
	 */
	public Link withName(String name) {

		return this.name == name ? this
				: new Link(this.rel, this.href, this.hreflang, this.media, this.title, this.type, this.deprecation,
						this.profile, name, this.template, this.affordances);
	}

	@JsonProperty
	public LinkRelation getRel() {
		return this.rel;
	}

	@JsonProperty
	public String getHref() {
		return this.href;
	}

	@Nullable
	@JsonProperty
	public String getHreflang() {
		return this.hreflang;
	}

	@Nullable
	@JsonProperty
	public String getMedia() {
		return this.media;
	}

	@Nullable
	@JsonProperty
	public String getTitle() {
		return this.title;
	}

	@Nullable
	@JsonProperty
	public String getType() {
		return this.type;
	}

	@Nullable
	@JsonProperty
	public String getDeprecation() {
		return this.deprecation;
	}

	@Nullable
	@JsonProperty
	public String getProfile() {
		return this.profile;
	}

	@Nullable
	@JsonProperty
	public String getName() {
		return this.name;
	}

	@JsonProperty
	public UriTemplate getTemplate() {

		UriTemplate template = this.template;

		return template == null ? UriTemplate.of(href) : template;
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#equals(java.lang.Object)
	 */
	@Override
	public boolean equals(@Nullable Object o) {

		if (this == o) {
			return true;
		}
		if (o == null || getClass() != o.getClass()) {
			return false;
		}
		Link link = (Link) o;
		return Objects.equals(this.rel, link.rel) && Objects.equals(this.href, link.href)
				&& Objects.equals(this.hreflang, link.hreflang) && Objects.equals(this.media, link.media)
				&& Objects.equals(this.title, link.title) && Objects.equals(this.type, link.type)
				&& Objects.equals(this.deprecation, link.deprecation) && Objects.equals(this.profile, link.profile)
				&& Objects.equals(this.name, link.name) && Objects.equals(this.affordances, link.affordances);
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#hashCode()
	 */
	@Override
	public int hashCode() {

		return Objects.hash(this.rel, this.href, this.hreflang, this.media, this.title, this.type, this.deprecation,
				this.profile, this.name, this.affordances);
	}

	/*
	 * (non-Javadoc)
	 * @see java.lang.Object#toString()
	 */
	@Override
	public String toString() {

		String linkString = String.format("<%s>;rel=\"%s\"", href, rel.value());

		if (hreflang != null) {
			linkString += ";hreflang=\"" + hreflang + "\"";
		}

		if (media != null) {
			linkString += ";media=\"" + media + "\"";
		}

		if (title != null) {
			linkString += ";title=\"" + title + "\"";
		}

		if (type != null) {
			linkString += ";type=\"" + type + "\"";
		}

		if (deprecation != null) {
			linkString += ";deprecation=\"" + deprecation + "\"";
		}

		if (profile != null) {
			linkString += ";profile=\"" + profile + "\"";
		}

		if (name != null) {
			linkString += ";name=\"" + name + "\"";
		}

		return linkString;
	}

	@Nullable
	private static UriTemplate templateOrNull(String href) {

		Assert.notNull(href, "Href must not be null!");

		return href.contains("{") ? UriTemplate.of(href) : null;
	}
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy