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

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

/*
 * Copyright 2013-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.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

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

import com.fasterxml.jackson.annotation.JsonValue;

/**
 * Value object to represent a list of {@link Link}s.
 *
 * @author Oliver Gierke
 * @author Greg Turnquist
 */
public class Links implements Iterable {

	public static final Links NONE = new Links(Collections.emptyList());
	private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("(<[^>]*>(;\\s*\\w+=\"?[^\",]*\"?)+)");

	private final List links;

	private Links(Iterable links) {

		Assert.notNull(links, "Links must not be null!");

		this.links = StreamSupport.stream(links.spliterator(), false) //
				.collect(Collectors.toList());
	}

	private Links(Link... links) {
		this(Arrays.asList(links));
	}

	/**
	 * Creates a new {@link Links} instance from the given {@link Link}s.
	 *
	 * @param links
	 */
	public static Links of(Link... links) {
		return new Links(links);
	}

	/**
	 * Creates a new {@link Links} instance from the given {@link Link}s.
	 *
	 * @param links
	 */
	public static Links of(Iterable links) {
		return Links.class.isInstance(links) ? Links.class.cast(links) : new Links(links);
	}

	/**
	 * Creates a {@link Links} instance from the given RFC-8288-compatible link format.
	 *
	 * @param source a comma separated list of {@link Link} representations.
	 * @return the {@link Links} represented by the given {@link String}.
	 */
	public static Links parse(@Nullable String source) {

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

		var links = new ArrayList();
		var matcher = LINK_HEADER_PATTERN.matcher(source);

		while (matcher.find()) {

			var link = Link.valueOf(matcher.group());

			if (link != null) {
				links.add(link);
			}
		}

		return new Links(links);
	}

	/**
	 * Creates a new {@link Links} instance with all given {@link Link}s added. For conditional adding see
	 * {@link #merge(Link...)}.
	 *
	 * @param links must not be {@literal null}.
	 * @return
	 * @see #merge(Link...)
	 * @see #merge(MergeMode, Link...)
	 */
	public Links and(Link... links) {

		Assert.notNull(links, "Links must not be null!");

		return and(Arrays.asList(links));
	}

	/**
	 * Adds the given links if the given condition is {@literal true}. The given {@link Link}s will only be resolved if
	 * the given condition is {@literal true}. Essentially syntactic sugar to write:
* * if (a > 3) { * links = links.and(…); * } * as * links = link.andIf(a > 3, …); * * * @param condition * @param links must not be {@literal null}. * @return */ @SafeVarargs public final Links andIf(boolean condition, Link... links) { return condition ? and(links) : this; } /** * Adds the given links if the given condition is {@literal true}. The given {@link Supplier}s will only be resolved * if the given condition is {@literal true}. Essentially syntactic sugar to write:
* * if (a > 3) { * links = links.and(…); * } * as * links = link.andIf(a > 3, …); * * * @param condition * @param links must not be {@literal null}. * @return */ @SafeVarargs public final Links andIf(boolean condition, Supplier... links) { Assert.notNull(links, "Links must not be null!"); return andIf(condition, Stream.of(links).map(Supplier::get)); } /** * Adds the given links if the given condition is {@literal true}. The given {@link Stream} will only be resolved if * the given condition is {@literal true}. Essentially syntactic sugar to write:
* * if (a > 3) { * links = links.and(…); * } * as * links = link.andIf(a > 3, …); * * * @param condition * @param links must not be {@literal null}. * @return */ public final Links andIf(boolean condition, Stream links) { Assert.notNull(links, "Links must not be null!"); return condition ? and(links.collect(Collectors.toList())) : this; } /** * Creates a new {@link Links} instance with all given {@link Link}s added. For conditional adding see * {@link #merge(Iterable)}. * * @param links must not be {@literal null}. * @return * @see #merge(Iterable) * @see #merge(MergeMode, Iterable) */ public Links and(Iterable links) { List newLinks = new ArrayList<>(this.links); links.forEach(newLinks::add); return Links.of(newLinks); } /** * Creates a new {@link Links} instance with all given {@link Link}s added. For conditional adding see * {@link #merge(Iterable)}. * * @param links must not be {@literal null}. * @return * @see #merge(Iterable) * @see #merge(MergeMode, Iterable) */ public Links and(Stream links) { return and(links.collect(Collectors.toList())); } /** * Merges the current {@link Links} with the given ones, skipping {@link Link}s already contained in the current * instance. For unconditional combination see {@link #and(Link...)}. * * @param links the {@link Link}s to be merged, must not be {@literal null}. * @return * @see MergeMode#SKIP_BY_EQUALITY * @see #and(Link...) */ public Links merge(Link... links) { return merge(Arrays.asList(links)); } /** * Merges the current {@link Links} with the given ones, skipping {@link Link}s already contained in the current * instance. For unconditional combination see {@link #and(Stream)}. * * @param links the {@link Link}s to be merged, must not be {@literal null}. * @return * @see MergeMode#SKIP_BY_EQUALITY * @see #and(Stream) */ public Links merge(Stream links) { return merge(links.collect(Collectors.toList())); } /** * Merges the current {@link Links} with the given ones, skipping {@link Link}s already contained in the current * instance. For unconditional combination see {@link #and(Iterable)}. * * @param links the {@link Link}s to be merged, must not be {@literal null}. * @return * @see MergeMode#SKIP_BY_EQUALITY * @see #and(Iterable) */ public Links merge(Iterable links) { return merge(MergeMode.SKIP_BY_EQUALITY, links); } /** * Merges the current {@link Links} with the given ones applying the given {@link MergeMode}. * * @param mode must not be {@literal null}. * @param links must not be {@literal null}. * @return */ public Links merge(MergeMode mode, Link... links) { return merge(mode, Arrays.asList(links)); } /** * Merges the current {@link Links} with the given ones applying the given {@link MergeMode}. * * @param mode must not be {@literal null}. * @param links must not be {@literal null}. * @return */ public Links merge(MergeMode mode, Stream links) { return merge(mode, links.collect(Collectors.toList())); } /** * Merges the current {@link Links} with the given ones applying the given {@link MergeMode}. * * @param mode must not be {@literal null}. * @param links must not be {@literal null}. * @return */ public Links merge(MergeMode mode, Iterable links) { Assert.notNull(mode, "MergeMode must not be null!"); Assert.notNull(links, "Links must not be null!"); List newLinks = MergeMode.REPLACE_BY_REL.equals(mode) // ? allWithoutRels(links) : new ArrayList<>(this.links); links.forEach(it -> { if (MergeMode.REPLACE_BY_REL.equals(mode)) { newLinks.add(it); } if (MergeMode.SKIP_BY_EQUALITY.equals(mode) && !this.links.contains(it)) { newLinks.add(it); } if (MergeMode.SKIP_BY_REL.equals(mode) && !this.hasLink(it.getRel())) { newLinks.add(it); } }); return new Links(newLinks); } /** * Returns a {@link Links} with all {@link Link}s with the given {@link LinkRelation} removed. * * @param relation must not be {@literal null}. * @return */ public Links without(LinkRelation relation) { Assert.notNull(relation, "LinkRelation must not be null!"); return this.links.stream() // .filter(it -> !it.hasRel(relation)) // .collect(Links.collector()); } /** * Returns a {@link Link} with the given relation if contained in the current {@link Links} instance, * {@link Optional#empty()} otherwise. * * @param relation must not be {@literal null} or empty. * @return */ public Optional getLink(String relation) { return getLink(LinkRelation.of(relation)); } /** * Returns the {@link Link} with the given rel. * * @param rel the relation type to lookup a link for. * @return the link with the given rel or {@literal Optional#empty()} if none found. */ public Optional getLink(LinkRelation rel) { return links.stream() // .filter(it -> it.hasRel(rel)) // .findFirst(); } /** * Returns the {@link Link} with the given relation. * * @param rel the relation type to lookup a link for. * @return * @throws IllegalArgumentException if no link with the given relation was present. * @since 1.0 */ public Link getRequiredLink(String rel) { return getRequiredLink(LinkRelation.of(rel)); } /** * Returns the {@link Link} with the given relation. * * @param relation the relation type to lookup a link for. * @return * @throws IllegalArgumentException if no link with the given relation was present. */ public Link getRequiredLink(LinkRelation relation) { return getLink(relation) // .orElseThrow(() -> new IllegalArgumentException(String.format("Couldn't find link with rel '%s'!", relation))); } /** * Returns whether the {@link Links} container contains a {@link Link} with the given relation. * * @param relation must not be {@literal null} or empty. * @return */ public boolean hasLink(String relation) { return getLink(relation).isPresent(); } /** * Returns whether the current {@link Links} contains a {@link Link} with the given relation. * * @param relation must not be {@literal null}. * @return */ public boolean hasLink(LinkRelation relation) { return getLink(relation).isPresent(); } /** * Returns whether the {@link Links} container is empty. * * @return */ public boolean isEmpty() { return links.isEmpty(); } /** * Returns whether the current {@link Links} has the given size. * * @param size * @return */ public boolean hasSize(long size) { return links.size() == size; } /** * Returns whether the {@link Links} contain a single {@link Link}. * * @return */ public boolean hasSingleLink() { return hasSize(1); } /** * Creates a {@link Stream} of the current {@link Links}. * * @return */ public Stream stream() { return this.links.stream(); } /** * Returns the current {@link Links} as {@link List}. * * @return */ @JsonValue public List toList() { return this.links; } /** * Returns whether the current {@link Links} contain all given {@link Link}s (but potentially others). * * @param links must not be {@literal null}. * @return */ public boolean contains(Link... links) { return this.links.containsAll(Links.of(links).toList()); } /** * Returns whether the current {@link Links} contain all given {@link Link}s (but potentially others). * * @param links must not be {@literal null}. * @return */ public boolean contains(Iterable links) { return this.links.containsAll(Links.of(links).toList()); } /** * Returns whether the current {@link Links} instance contains exactly the same {@link Link} as the given one. * * @param links must not be {@literal null}. * @return */ public boolean containsSameLinksAs(Iterable links) { Links other = Links.of(links); return this.links.size() != other.links.size() ? false : this.links.containsAll(other.links); } /** * Creates a new {@link Collector} to collect a {@link Stream} of {@link Link}s into a {@link Links} instance. * * @return will never be {@literal null}. */ public static Collector collector() { return Collectors.collectingAndThen(Collectors.toList(), Links::of); } /* * (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return StringUtils.collectionToCommaDelimitedString(links); } /* * (non-Javadoc) * @see java.lang.Iterable#iterator() */ @Override public Iterator iterator() { return links.iterator(); } /* * (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof Links)) { return false; } Links that = (Links) obj; return this.links.equals(that.links); } /* * (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { int result = 17; result += 31 * links.hashCode(); return result; } private List allWithoutRels(Iterable links) { Set toFilter = StreamSupport.stream(links.spliterator(), false) // .map(Link::getRel) // .collect(Collectors.toSet()); return this.links.stream() // .filter(it -> !toFilter.contains(it.getRel())) // .collect(Collectors.toList()); } /** * The mode how to merge two {@link Links} instances. * * @author Oliver Drotbohm */ public enum MergeMode { /** * Skips to add the same links on merge. Multiple links with the same link relation might appear. */ SKIP_BY_EQUALITY, /** * Skips to add links with the same link relation, i.e. existing ones with the same relation are preferred. */ SKIP_BY_REL, /** * Replaces existing links with the same link relation. */ REPLACE_BY_REL } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy