#812 - General overhaul and refactorings.

Cleanups in Affordance API and implementations of hypermedia type (de)serializers. Added a lot more domain methods and types to Link, ResourceSupport etc. to be able to move a lot of representation building logic into those.

AffordanceModelFactory is not a Spring Plugin anymore as that functionality is not needed currently as we statically look up all factories via the SpringFactoriesLoader mechanism.

Redesigned LinkRelation to become a first class abstraction in the codebase. IanaLinkRelations is now a collection of constants. Link now keeps a LinkRelation instance around instead of a plain String.

Tweaked LinkDiscoverer API to return Optional and Links instead of nullable Link and List<Link>.

Tweaked API of CurieProvider to make use of the newly introduced HalLinkRelation based on the general LinkRelation.

Additional fixes for the Kotlin extension functions. Make Kotlin build setup compile with JDK 8. Removed Objects helper class in favor of Spring's already existing Assert and it's usage in ResourceAssemblerSupport.
This commit is contained in:
Oliver Drotbohm
2019-02-12 22:04:36 +01:00
committed by Oliver Drotbohm
parent 38b98af786
commit 9e5db1874e
116 changed files with 3282 additions and 2594 deletions

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.hateoas;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.Value;
import java.util.HashMap;
@@ -42,8 +44,18 @@ public class Affordance {
/**
* Collection of {@link AffordanceModel}s related to this affordance.
*/
private final Map<MediaType, AffordanceModel> affordanceModels = new HashMap<>();
private final @Getter(AccessLevel.PACKAGE) Map<MediaType, AffordanceModel> affordanceModels = new HashMap<>();
/**
* Creates a new {@link Affordance}.
*
* @param name
* @param link
* @param httpMethod
* @param inputType
* @param queryMethodParameters
* @param outputType
*/
public Affordance(String name, Link link, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType) {

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 the original author or authors.
* Copyright 2018-2019 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.
@@ -23,11 +23,13 @@ import java.util.List;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.util.Assert;
/**
* Collection of attributes needed to render any form of hypermedia.
*
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@AllArgsConstructor
@@ -58,7 +60,7 @@ public abstract class AffordanceModel {
* Collection of {@link QueryParameter}s to interrogate a resource.
*/
private List<QueryParameter> queryMethodParameters;
/**
* Response body domain type.
*/
@@ -72,4 +74,30 @@ public abstract class AffordanceModel {
public String getURI() {
return this.link.expand().getHref();
}
/**
* Returns whether the {@link Affordance} has the given {@link HttpMethod}.
*
* @param method must not be {@literal null}.
* @return
*/
public boolean hasHttpMethod(HttpMethod method) {
Assert.notNull(method, "HttpMethod must not be null!");
return this.httpMethod.equals(method);
}
/**
* Returns whether the {@link Affordance} points to the target of the given {@link Link}.
*
* @param link must not be {@literal null}.
* @return
*/
public boolean pointsToTargetOf(Link link) {
Assert.notNull(link, "Link must not be null!");
return getURI().equals(link.expand().getHref());
}
}

View File

@@ -16,27 +16,23 @@
package org.springframework.hateoas;
import java.util.List;
import java.util.Optional;
import org.springframework.core.ResolvableType;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.Plugin;
/**
* @author Greg Turnquist
* @author Oliver Gierke
*/
public interface AffordanceModelFactory extends Plugin<MediaType> {
public interface AffordanceModelFactory {
/**
* Declare the {@link MediaType} this factory supports.
*
* @return
*/
default MediaType getMediaType() {
return null;
}
MediaType getMediaType();
/**
* Look up the {@link AffordanceModel} for this factory.
@@ -51,18 +47,4 @@ public interface AffordanceModelFactory extends Plugin<MediaType> {
*/
AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType);
/**
* Returns if a plugin should be invoked according to the given delimiter.
*
* @param delimiter
* @return if the plugin should be invoked
*/
@Override
default boolean supports(MediaType delimiter) {
return Optional.ofNullable(getMediaType()) //
.map(mediaType -> mediaType.equals(delimiter)) //
.orElse(false);
}
}

View File

@@ -71,7 +71,7 @@ public interface EntityLinks extends Plugin<Class<?>> {
/**
* Creates a {@link Link} pointing to the collection resource of the given type. The relation type of the link will be
* determined by the implementation class and should be defaulted to {@link IanaLinkRelation#SELF}.
* determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}.
*
* @param type the entity type to point to, must not be {@literal null}.
* @return the {@link Link} pointing to the collection resource exposed for the given entity. Will never be
@@ -82,7 +82,7 @@ public interface EntityLinks extends Plugin<Class<?>> {
/**
* Creates a {@link Link} pointing to single resource backing the given entity type and id. The relation type of the
* link will be determined by the implementation class and should be defaulted to {@link IanaLinkRelation#SELF}.
* link will be determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}.
*
* @param type the entity type to point to, must not be {@literal null}.
* @param id the identifier of the entity of the given type
@@ -94,7 +94,7 @@ public interface EntityLinks extends Plugin<Class<?>> {
/**
* Creates a {@link Link} pointing to single resource backing the given entity. The relation type of the link will be
* determined by the implementation class and should be defaulted to {@link IanaLinkRelation#SELF}.
* determined by the implementation class and should be defaulted to {@link IanaLinkRelations#SELF}.
*
* @param entity the entity type to point to, must not be {@literal null}.
* @return the {@link Link} pointing to the resource exposed for the given entity. Will never be {@literal null}.

View File

@@ -15,167 +15,172 @@
*/
package org.springframework.hateoas;
import lombok.experimental.UtilityClass;
import java.util.Arrays;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.util.ReflectionUtils;
/**
* Capture standard IANA-based link relations.
*
* @see https://www.iana.org/assignments/link-relations/link-relations.xhtml
* @see https://tools.ietf.org/html/rfc8288
* @see https://github.com/link-relations/registry
*
* @author Greg Turnquist
* @author Roland Kulcsár
* @author Oliver Gierke
* @since 1.0
*/
public enum IanaLinkRelation implements LinkRelation {
@UtilityClass
public class IanaLinkRelations {
/**
* Refers to a resource that is the subject of the link's context.
*
* @see https://tools.ietf.org/html/rfc6903
*/
ABOUT("about"),
public static final LinkRelation ABOUT = LinkRelation.of("about");
/**
* Refers to a substitute for this context
*
* @see http://www.w3.org/TR/html5/links.html#link-type-alternate
*/
ALTERNATE("alternate"),
public static final LinkRelation ALTERNATE = LinkRelation.of("alternate");
/**
* Refers to an appendix.
*
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
APPENDIX("appendix"),
public static final LinkRelation APPENDIX = LinkRelation.of("appendix");
/**
* Refers to a collection of records, documents, or other materials of historical interest.
*
* @see http://www.w3.org/TR/2011/WD-html5-20110113/links.html#rel-archives
*/
ARCHIVES("archives"),
public static final LinkRelation ARCHIVES = LinkRelation.of("archives");
/**
* Refers to the context's author.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-author
*/
AUTHOR("author"),
public static final LinkRelation AUTHOR = LinkRelation.of("author");
/**
* Identifies the entity that blocks access to a resource following receipt of a legal demand.
*
*
* @see https://tools.ietf.org/html/rfc7725
*/
BLOCKED_BY("blocked-by"),
public static final LinkRelation BLOCKED_BY = LinkRelation.of("blocked-by");
/**
* Gives a permanent link to use for bookmarking purposes.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-bookmark
*/
BOOKMARK("bookmark"),
public static final LinkRelation BOOKMARK = LinkRelation.of("bookmark");
/**
* Designates the preferred version of a resource (the IRI and its contents).
*
* @see https://tools.ietf.org/html/rfc6596
*/
CANONICAL("canonical"),
public static final LinkRelation CANONICAL = LinkRelation.of("canonical");
/**
* Refers to a chapter in a collection of resources.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
CHAPTER("chapter"),
public static final LinkRelation CHAPTER = LinkRelation.of("chapter");
/**
* Indicates that the link target is preferred over the link context for the purpose of referencing.
*
* @see https://datatracker.ietf.org/doc/draft-vandesompel-citeas/
*/
CITE_AS("cite-as"),
public static final LinkRelation CITE_AS = LinkRelation.of("cite-as");
/**
* The target IRI points to a resource which represents the collection resource for the context IRI.
*
* @see https://tools.ietf.org/html/rfc6573
*/
COLLECTION("collection"),
public static final LinkRelation COLLECTION = LinkRelation.of("collection");
/**
* Refers to a table of contents.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
CONTENTS("contents"),
public static final LinkRelation CONTENTS = LinkRelation.of("contents");
/**
* The document linked to was later converted to the document that contains this link relation.
* For example, an RFC can have a link to the Internet-Draft that became the RFC; in that case,
* the link relation would be "convertedFrom".
*
* The document linked to was later converted to the document that contains this link relation. For example, an RFC
* can have a link to the Internet-Draft that became the RFC; in that case, the link relation would be
* "convertedFrom".
*
* @see https://tools.ietf.org/html/rfc7991
*/
CONVERTED_FROM("convertedFrom"),
public static final LinkRelation CONVERTED_FROM = LinkRelation.of("convertedFrom");
/**
* Refers to a copyright statement that applies to the link's context.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
COPYRIGHT("copyright"),
public static final LinkRelation COPYRIGHT = LinkRelation.of("copyright");
/**
* The target IRI points to a resource where a submission form can be obtained.
*
* @see https://tools.ietf.org/html/rfc6861
*/
CREATE_FORM("create-form"),
public static final LinkRelation CREATE_FORM = LinkRelation.of("create-form");
/**
* Refers to a resource containing the most recent item(s) in a collection of resources.
*
* @see https://tools.ietf.org/html/rfc5005
*/
CURRENT("current"),
public static final LinkRelation CURRENT = LinkRelation.of("current");
/**
* Refers to a resource providing information about the link's context.
*
* @see http://www.w3.org/TR/powder-dr/#assoc-linking
*/
DESCRIBED_BY("describedBy"),
public static final LinkRelation DESCRIBED_BY = LinkRelation.of("describedBy");
/**
* The relationship A 'describes' B asserts that resource A provides a description of resource B. There are no
* constraints on the format or representation of either A or B, neither are there any further constraints on
* either resource.
* constraints on the format or representation of either A or B, neither are there any further constraints on either
* resource.
*
* @see https://tools.ietf.org/html/rfc6892
*/
DESCRIBES("describes"),
public static final LinkRelation DESCRIBES = LinkRelation.of("describes");
/**
* Refers to a list of patent disclosures made with respect to material for which 'disclosure' relation is
* specified.
* Refers to a list of patent disclosures made with respect to material for which 'disclosure' relation is specified.
*
* @see https://tools.ietf.org/html/rfc6579
*/
DISCLOSURE("disclosure"),
public static final LinkRelation DISCLOSURE = LinkRelation.of("disclosure");
/**
* Used to indicate an origin that will be used to fetch required resources for the link context, and that the
* user agent ought to resolve as early as possible.
* Used to indicate an origin that will be used to fetch required resources for the link context, and that the user
* agent ought to resolve as early as possible.
*
* @see https://www.w3.org/TR/resource-hints/
*/
DNS_PREFETCH("dns-prefetch"),
public static final LinkRelation DNS_PREFETCH = LinkRelation.of("dns-prefetch");
/**
* Refers to a resource whose available representations are byte-for-byte identical with the corresponding
@@ -183,100 +188,100 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see https://tools.ietf.org/html/rfc6249
*/
DUPLICATE("duplicate"),
public static final LinkRelation DUPLICATE = LinkRelation.of("duplicate");
/**
* Refers to a resource that can be used to edit the link's context.
*
* @see https://tools.ietf.org/html/rfc5023
*/
EDIT("edit"),
public static final LinkRelation EDIT = LinkRelation.of("edit");
/**
* The target IRI points to a resource where a submission form for editing associated resource can be obtained.
*
* @see https://tools.ietf.org/html/rfc6861
*/
EDIT_FORM("edit-form"),
public static final LinkRelation EDIT_FORM = LinkRelation.of("edit-form");
/**
* Refers to a resource that can be used to edit media associated with the link's context.
*
* @see https://tools.ietf.org/html/rfc5023
*/
EDIT_MEDIA("edit-media"),
public static final LinkRelation EDIT_MEDIA = LinkRelation.of("edit-media");
/**
* Identifies a related resource that is potentially large and might require special handling.
*
* @see https://tools.ietf.org/html/rfc4287
*/
ENCLOSURE("enclosure"),
public static final LinkRelation ENCLOSURE = LinkRelation.of("enclosure");
/**
* An IRI that refers to the furthest preceding resource in a series of resources.
*
* @see https://tools.ietf.org/html/rfc8288
*/
FIRST("first"),
public static final LinkRelation FIRST = LinkRelation.of("first");
/**
* Refers to a glossary of terms.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
GLOSSARY("glossary"),
public static final LinkRelation GLOSSARY = LinkRelation.of("glossary");
/**
* Refers to context-sensitive help.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-help
*/
HELP("help"),
public static final LinkRelation HELP = LinkRelation.of("help");
/**
* Refers to a resource hosted by the server indicated by the link context.
*
* @see https://tools.ietf.org/html/rfc6690
*/
HOSTS("hosts"),
public static final LinkRelation HOSTS = LinkRelation.of("hosts");
/**
* Refers to a hub that enables registration for notification of updates to the context.
*
* @see http://pubsubhubbub.googlecode.com
*/
HUB("hub"),
public static final LinkRelation HUB = LinkRelation.of("hub");
/**
* Refers to an icon representing the link's context.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-icon
*/
ICON("icon"),
public static final LinkRelation ICON = LinkRelation.of("icon");
/**
* Refers to an index.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
INDEX("index"),
public static final LinkRelation INDEX = LinkRelation.of("index");
/**
* refers to a resource associated with a time interval that ends before the beginning of the time interval
* associated with the context resource
* refers to a resource associated with a time interval that ends before the beginning of the time interval associated
* with the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalAfter section 4.2.21
*/
INTERVAL_AFTER("intervalAfter"),
public static final LinkRelation INTERVAL_AFTER = LinkRelation.of("intervalAfter");
/**
* refers to a resource associated with a time interval that begins after the end of the time interval associated
* with the context resource
* refers to a resource associated with a time interval that begins after the end of the time interval associated with
* the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalBefore section 4.2.22
*/
INTERVAL_BEFORE("intervalBefore"),
public static final LinkRelation INTERVAL_BEFORE = LinkRelation.of("intervalBefore");
/**
* refers to a resource associated with a time interval that begins after the beginning of the time interval
@@ -285,15 +290,15 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see https://www.w3.org/TR/owl-time/#time:intervalContains section 4.2.23
*/
INTERVAL_CONTAINS("intervalContains"),
public static final LinkRelation INTERVAL_CONTAINS = LinkRelation.of("intervalContains");
/**
* refers to a resource associated with a time interval that begins after the end of the time interval associated
* with the context resource, or ends before the beginning of the time interval associated with the context resource
* refers to a resource associated with a time interval that begins after the end of the time interval associated with
* the context resource, or ends before the beginning of the time interval associated with the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalDisjoint section 4.2.24
*/
INTERVAL_DISJOINT("intervalDisjoint"),
public static final LinkRelation INTERVAL_DISJOINT = LinkRelation.of("intervalDisjoint");
/**
* refers to a resource associated with a time interval that begins before the beginning of the time interval
@@ -302,68 +307,68 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see https://www.w3.org/TR/owl-time/#time:intervalDuring section 4.2.25
*/
INTERVAL_DURING("intervalDuring"),
public static final LinkRelation INTERVAL_DURING = LinkRelation.of("intervalDuring");
/**
* refers to a resource associated with a time interval whose beginning coincides with the beginning of the time
* interval associated with the context resource, and whose end coincides with the end of the time interval
* associated with the context resource
* interval associated with the context resource, and whose end coincides with the end of the time interval associated
* with the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalEquals section 4.2.26
*/
INTERVAL_EQUALS("intervalEquals"),
public static final LinkRelation INTERVAL_EQUALS = LinkRelation.of("intervalEquals");
/**
* refers to a resource associated with a time interval that begins after the beginning of the time interval
* associated with the context resource, and whose end coincides with the end of the time interval associated with
* the context resource
* associated with the context resource, and whose end coincides with the end of the time interval associated with the
* context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalFinishedBy section 4.2.27
*/
INTERVAL_FINISHED_BY("intervalFinishedBy"),
public static final LinkRelation INTERVAL_FINISHED_BY = LinkRelation.of("intervalFinishedBy");
/**
* refers to a resource associated with a time interval that begins before the beginning of the time interval
* associated with the context resource, and whose end coincides with the end of the time interval associated with
* the context resource
* associated with the context resource, and whose end coincides with the end of the time interval associated with the
* context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalFinishes section 4.2.28
*/
INTERVAL_FINISHES("intervalFinishes"),
public static final LinkRelation INTERVAL_FINISHES = LinkRelation.of("intervalFinishes");
/**
* refers to a resource associated with a time interval that begins before or is coincident with the beginning
* of the time interval associated with the context resource, and ends after or is coincident with the end of the
* time interval associated with the context resource
* refers to a resource associated with a time interval that begins before or is coincident with the beginning of the
* time interval associated with the context resource, and ends after or is coincident with the end of the time
* interval associated with the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalIn section 4.2.29
*/
INTERVAL_IN("intervalIn"),
public static final LinkRelation INTERVAL_IN = LinkRelation.of("intervalIn");
/**
* refers to a resource associated with a time interval whose beginning coincides with the end of the time
* interval associated with the context resource
* refers to a resource associated with a time interval whose beginning coincides with the end of the time interval
* associated with the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalMeets section 4.2.30
*/
INTERVAL_MEETS("intervalMeets"),
public static final LinkRelation INTERVAL_MEETS = LinkRelation.of("intervalMeets");
/**
* refers to a resource associated with a time interval whose beginning coincides with the end of the time
* interval associated with the context resource
* refers to a resource associated with a time interval whose beginning coincides with the end of the time interval
* associated with the context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalMetBy section 4.2.31
*/
INTERVAL_MET_BY("intervalMetBy"),
public static final LinkRelation INTERVAL_MET_BY = LinkRelation.of("intervalMetBy");
/**
* refers to a resource associated with a time interval that begins before the beginning of the time interval
* associated with the context resource, and ends after the beginning of the time interval associated with the
* context resource
* associated with the context resource, and ends after the beginning of the time interval associated with the context
* resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalOverlappedBy section 4.2.32
*/
INTERVAL_OVERLAPPED_BY("intervalOverlappedBy"),
public static final LinkRelation INTERVAL_OVERLAPPED_BY = LinkRelation.of("intervalOverlappedBy");
/**
* refers to a resource associated with a time interval that begins before the end of the time interval associated
@@ -371,16 +376,16 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see https://www.w3.org/TR/owl-time/#time:intervalOverlaps section 4.2.33
*/
INTERVAL_OVERLAPS("intervalOverlaps"),
public static final LinkRelation INTERVAL_OVERLAPS = LinkRelation.of("intervalOverlaps");
/**
* refers to a resource associated with a time interval whose beginning coincides with the beginning of the time
* interval associated with the context resource, and ends before the end of the time interval associated with
* the context resource
* interval associated with the context resource, and ends before the end of the time interval associated with the
* context resource
*
* @see https://www.w3.org/TR/owl-time/#time:intervalStartedBy section 4.2.34
*/
INTERVAL_STARTED_BY("intervalStartedBy"),
public static final LinkRelation INTERVAL_STARTED_BY = LinkRelation.of("intervalStartedBy");
/**
* refers to a resource associated with a time interval whose beginning coincides with the beginning of the time
@@ -389,140 +394,140 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see https://www.w3.org/TR/owl-time/#time:intervalStarts section 4.2.35
*/
INTERVAL_STARTS("intervalStarts"),
public static final LinkRelation INTERVAL_STARTS = LinkRelation.of("intervalStarts");
/**
* The target IRI points to a resource that is a member of the collection represented by the context IRI.
*
* @see https://tools.ietf.org/html/rfc6573
*/
ITEM("item"),
public static final LinkRelation ITEM = LinkRelation.of("item");
/**
* An IRI that refers to the furthest following resource in a series of resources.
*
* @see https://tools.ietf.org/html/rfc8288
*/
LAST("last"),
public static final LinkRelation LAST = LinkRelation.of("last");
/**
* Points to a resource containing the latest (e.g., current) version of the context.
*
* @see https://tools.ietf.org/html/rfc5829
*/
LATEST_VERSION("latest-version"),
public static final LinkRelation LATEST_VERSION = LinkRelation.of("latest-version");
/**
* Refers to a license associated with this context.
*
* @see https://tools.ietf.org/html/rfc4946
*/
LICENSE("license"),
public static final LinkRelation LICENSE = LinkRelation.of("license");
/**
* Refers to further information about the link's context, expressed as a LRDD ("Link-based Resource Descriptor
* Document") resource. See RFC6415 for information about processing this relation type in host-meta documents.
* When used elsewhere, it refers to additional links and other metadata. Multiple instances indicate additional
* LRDD resources. LRDD resources MUST have an "application/xrd+xml" representation, and MAY have others.
* Document") resource. See RFC6415 for information about processing this relation type in host-meta documents. When
* used elsewhere, it refers to additional links and other metadata. Multiple instances indicate additional LRDD
* resources. LRDD resources MUST have an "application/xrd+xml" representation, and MAY have others.
*
* @see https://tools.ietf.org/html/rfc6415
*/
LRDD("lrdd"),
public static final LinkRelation LRDD = LinkRelation.of("lrdd");
/**
* The Target IRI points to a Memento, a fixed resource that will not change state anymore.
*
* @see https://tools.ietf.org/html/rfc7089
*/
MEMENTO("memento"),
public static final LinkRelation MEMENTO = LinkRelation.of("memento");
/**
* Refers to a resource that can be used to monitor changes in an HTTP resource.
*
* @see https://tools.ietf.org/html/rfc5989
*/
MONITOR("monitor"),
public static final LinkRelation MONITOR = LinkRelation.of("monitor");
/**
* Refers to a resource that can be used to monitor changes in a specified group of HTTP resources.
*
* @see https://tools.ietf.org/html/rfc5989
*/
MONITOR_GROUP("monitor-group"),
public static final LinkRelation MONITOR_GROUP = LinkRelation.of("monitor-group");
/**
* Indicates that the link's context is a part of a series, and that the next in the series is the link target.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-next
*/
NEXT("next"),
public static final LinkRelation NEXT = LinkRelation.of("next");
/**
* Refers to the immediately following archive resource.
*
* @see https://tools.ietf.org/html/rfc5005
*/
NEXT_ARCHIVE("next-archive"),
public static final LinkRelation NEXT_ARCHIVE = LinkRelation.of("next-archive");
/**
* Indicates that the contextÄôs original author or publisher does not endorse the link target.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-nofollow
*/
NOFOLLOW("nofollow"),
public static final LinkRelation NOFOLLOW = LinkRelation.of("nofollow");
/**
* Indicates that no referrer information is to be leaked when following the link.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-noreferrer
*/
NOREFERRER("noreferrer"),
public static final LinkRelation NOREFERRER = LinkRelation.of("noreferrer");
/**
* The Target IRI points to an Original Resource.
*
* @see https://tools.ietf.org/html/rfc7089
*/
ORIGINAL("original"),
public static final LinkRelation ORIGINAL = LinkRelation.of("original");
/**
* Indicates a resource where payment is accepted.
*
* @see https://tools.ietf.org/html/rfc8288
*/
PAYMENT("payment"),
public static final LinkRelation PAYMENT = LinkRelation.of("payment");
/**
* Gives the address of the pingback resource for the link context.
*
* @see http://www.hixie.ch/specs/pingback/pingback
*/
PINGBACK("pingback"),
public static final LinkRelation PINGBACK = LinkRelation.of("pingback");
/**
* Used to indicate an origin that will be used to fetch required resources for the link context. Initiating an
* early connection, which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allows the user
* agent to mask the high latency costs of establishing a connection.
* Used to indicate an origin that will be used to fetch required resources for the link context. Initiating an early
* connection, which includes the DNS lookup, TCP handshake, and optional TLS negotiation, allows the user agent to
* mask the high latency costs of establishing a connection.
*
* @see https://www.w3.org/TR/resource-hints/
*/
PRECONNECT("preconnect"),
public static final LinkRelation PRECONNECT = LinkRelation.of("preconnect");
/**
* Points to a resource containing the predecessor version in the version history.
*
* @see https://tools.ietf.org/html/rfc5829
*/
PREDECESSOR_VERSION("predecessor-version"),
public static final LinkRelation PREDECESSOR_VERSION = LinkRelation.of("predecessor-version");
/**
* The prefetch link relation type is used to identify a resource that might be required by the next navigation
* from the link context, and that the user agent ought to fetch, such that the user agent can deliver a faster
* response once the resource is requested in the future.
*
* The prefetch link relation type is used to identify a resource that might be required by the next navigation from
* the link context, and that the user agent ought to fetch, such that the user agent can deliver a faster response
* once the resource is requested in the future.
*
* @see http://www.w3.org/TR/resource-hints/
*/
PREFETCH("prefetch"),
public static final LinkRelation PREFETCH = LinkRelation.of("prefetch");
/**
* Refers to a resource that should be loaded early in the processing of the link's context, without blocking
@@ -530,51 +535,51 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see http://www.w3.org/TR/preload/
*/
PRELOAD("preload"),
public static final LinkRelation PRELOAD = LinkRelation.of("preload");
/**
* Used to identify a resource that might be required by the next navigation from the link context, and that the
* user agent ought to fetch and execute, such that the user agent can deliver a faster response once the resource
* is requested in the future.
* Used to identify a resource that might be required by the next navigation from the link context, and that the user
* agent ought to fetch and execute, such that the user agent can deliver a faster response once the resource is
* requested in the future.
*
* @see https://www.w3.org/TR/resource-hints/
*/
PRERENDER("prerender"),
public static final LinkRelation PRERENDER = LinkRelation.of("prerender");
/**
* Indicates that the link's context is a part of a series, and that the previous in the series is the link target.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-prev
*/
PREV("prev"),
public static final LinkRelation PREV = LinkRelation.of("prev");
/**
* Refers to a resource that provides a preview of the link's context.
*
* @see https://tools.ietf.org/html/rfc6903, section 3
*/
PREVIEW("preview"),
public static final LinkRelation PREVIEW = LinkRelation.of("preview");
/**
* Refers to the previous resource in an ordered series of resources. Synonym for "prev".
* Refers to the previous resource in an ordered series of resources. Synonym for "prev".
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
PREVIOUS("previous"),
public static final LinkRelation PREVIOUS = LinkRelation.of("previous");
/**
* Refers to the immediately preceding archive resource.
*
* @see https://tools.ietf.org/html/rfc5005
*/
PREV_ARCHIVE("prev-archive"),
public static final LinkRelation PREV_ARCHIVE = LinkRelation.of("prev-archive");
/**
* Refers to a privacy policy associated with the link's context.
*
* @see https://tools.ietf.org/html/rfc6903, section 4
*/
PRIVACY_POLICY("privacy-policy"),
public static final LinkRelation PRIVACY_POLICY = LinkRelation.of("privacy-policy");
/**
* Identifying that a resource representation conforms to a certain profile, without affecting the non-profile
@@ -582,253 +587,217 @@ public enum IanaLinkRelation implements LinkRelation {
*
* @see https://tools.ietf.org/html/rfc6906
*/
PROFILE("profile"),
public static final LinkRelation PROFILE = LinkRelation.of("profile");
/**
* Identifies a related resource.
*
* @see https://tools.ietf.org/html/rfc4287
*/
RELATED("related"),
public static final LinkRelation RELATED = LinkRelation.of("related");
/**
* Identifies the root of RESTCONF API as configured on this HTTP server. The "restconf" relation defines the
* root of the API defined in RFC8040. Subsequent revisions of RESTCONF will use alternate relation values to
* support protocol versioning.
* Identifies the root of RESTCONF API as configured on this HTTP server. The "restconf" relation defines the root of
* the API defined in RFC8040. Subsequent revisions of RESTCONF will use alternate relation values to support protocol
* versioning.
*
* @see https://tools.ietf.org/html/rfc8040
*/
RESTCONF("restconf"),
public static final LinkRelation RESTCONF = LinkRelation.of("restconf");
/**
* Identifies a resource that is a reply to the context of the link.
*
* @see https://tools.ietf.org/html/rfc4685
*/
REPLIES("replies"),
public static final LinkRelation REPLIES = LinkRelation.of("replies");
/**
* Refers to a resource that can be used to search through the link's context and related resources.
*
* @see http://www.opensearch.org/Specifications/OpenSearch/1.1
*/
SEARCH("search"),
public static final LinkRelation SEARCH = LinkRelation.of("search");
/**
* Refers to a section in a collection of resources.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
SECTION("section"),
public static final LinkRelation SECTION = LinkRelation.of("section");
/**
* Conveys an identifier for the link's context.
*
* @see https://tools.ietf.org/html/rfc4287
*/
SELF("self"),
public static final LinkRelation SELF = LinkRelation.of("self");
/**
* Indicates a URI that can be used to retrieve a service document.
*
* @see https://tools.ietf.org/html/rfc5023
*/
SERVICE("service"),
public static final LinkRelation SERVICE = LinkRelation.of("service");
/**
* Refers to the first resource in a collection of resources.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
START("start"),
public static final LinkRelation START = LinkRelation.of("start");
/**
* Refers to a stylesheet.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-stylesheet
*/
STYLESHEET("stylesheet"),
public static final LinkRelation STYLESHEET = LinkRelation.of("stylesheet");
/**
* Refers to a resource serving as a subsection in a collection of resources.
*
* @see http://www.w3.org/TR/1999/REC-html401-19991224
*/
SUBSECTION("subsection"),
public static final LinkRelation SUBSECTION = LinkRelation.of("subsection");
/**
* Points to a resource containing the successor version in the version history.
*
* @see https://tools.ietf.org/html/rfc5829
*/
SUCCESSOR_VERSION("successor-versions"),
public static final LinkRelation SUCCESSOR_VERSION = LinkRelation.of("successor-versions");
/**
* Gives a tag (identified by the given address) that applies to the current document.
*
* @see http://www.w3.org/TR/html5/links.html#link-type-tag
*/
TAG("tag"),
public static final LinkRelation TAG = LinkRelation.of("tag");
/**
* Refers to the terms of service associated with the link's context.
*
* @see https://tools.ietf.org/html/rfc6903, section 5
*/
TERMS_OF_SERVICE("terms-of-service"),
public static final LinkRelation TERMS_OF_SERVICE = LinkRelation.of("terms-of-service");
/**
* The Target IRI points to a TimeGate for an Original Resource.
*
* @see https://tools.ietf.org/html/rfc7089
*/
TIMEGATE("timegate"),
public static final LinkRelation TIMEGATE = LinkRelation.of("timegate");
/**
* The Target IRI points to a TimeMap for an Original Resource.
*
* @see https://tools.ietf.org/html/rfc7089
*/
TIMEMAP("timemap"),
public static final LinkRelation TIMEMAP = LinkRelation.of("timemap");
/**
* Refers to a resource identifying the abstract semantic type of which the link's context is considered to be
* an instance.
* Refers to a resource identifying the abstract semantic type of which the link's context is considered to be an
* instance.
*
* @see https://tools.ietf.org/html/rfc6903, section 6
*/
TYPE("type"),
public static final LinkRelation TYPE = LinkRelation.of("type");
/**
* Refers to a parent document in a hierarchy of documents.
*
* @see https://tools.ietf.org/html/rfc8288
*/
UP("up"),
public static final LinkRelation UP = LinkRelation.of("up");
/**
* Points to a resource containing the version history for the context.
*
* @see https://tools.ietf.org/html/rfc5829
*/
VERSION_HISTORY("version-history"),
public static final LinkRelation VERSION_HISTORY = LinkRelation.of("version-history");
/**
* Identifies a resource that is the source of the information in the link's context.
*
* @see https://tools.ietf.org/html/rfc4287
*/
VIA("via"),
public static final LinkRelation VIA = LinkRelation.of("via");
/**
* Identifies a target URI that supports the Webmention protcol. This allows clients that mention a resource in
* some form of publishing process to contact that endpoint and inform it that this resource has been mentioned.
* Identifies a target URI that supports the Webmention protcol. This allows clients that mention a resource in some
* form of publishing process to contact that endpoint and inform it that this resource has been mentioned.
*
* @see http://www.w3.org/TR/webmention/
*/
WEBMENTION("webmention"),
public static final LinkRelation WEBMENTION = LinkRelation.of("webmention");
/**
* Points to a working copy for this resource.
*
* @see https://tools.ietf.org/html/rfc5829
*/
WORKING_COPY("working-copy"),
public static final LinkRelation WORKING_COPY = LinkRelation.of("working-copy");
/**
* Points to the versioned resource from which this working copy was obtained.
*
*
* @see https://tools.ietf.org/html/rfc5829
*/
WORKING_COPY_OF("working-copy-of");
/**
* Actual IANA value for the link relation.
*/
private final String value;
public static final LinkRelation WORKING_COPY_OF = LinkRelation.of("working-copy-of");
/**
* Initialize the enum value.
*
* @param value
* Consolidated collection of {@link IanaLinkRelations}s.
*/
IanaLinkRelation(String value) {
this.value = value;
}
/**
* Return the IANA value.
*/
@Override
public String value() {
return this.value;
}
/**
* Consolidated collection of {@link IanaLinkRelation}s.
*/
public static final Set<IanaLinkRelation> LINK_RELATIONS;
/**
* Consolidated collection of {@link IanaLinkRelation} values.
*/
public static final Set<String> RELS;
private final Set<LinkRelation> LINK_RELATIONS;
static {
LINK_RELATIONS = Arrays.stream(IanaLinkRelation.values())
.collect(Collectors.toSet());
RELS = LINK_RELATIONS.stream()
.map(LinkRelation::value)
.collect(Collectors.toSet());
LINK_RELATIONS = Arrays.stream(IanaLinkRelations.class.getDeclaredFields()) //
.filter(ReflectionUtils::isPublicStaticFinal) //
.map(it -> ReflectionUtils.getField(it, null)) //
.map(LinkRelation.class::cast) //
.collect(Collectors.toSet());
}
/**
* Is this relation an IANA standard?
* Is this relation an IANA standard? Per RFC8288, parsing of link relations is case insensitive.
*
* Per RFC8288, parsing of link relations is case insensitive.
*
* @param rel
* @return boolean
*/
public static boolean isIanaRel(String rel) {
return
rel != null
&&
LINK_RELATIONS.stream().anyMatch(linkRelation -> linkRelation.value().equalsIgnoreCase(rel));
return rel != null && LINK_RELATIONS.stream() //
.anyMatch(it -> it.value().equalsIgnoreCase(rel));
}
/**
* Is this relation an IANA standard?
*
* Per RFC8288, parsing of link relations is case insensitive.
* Is this relation an IANA standard? Per RFC8288, parsing of link relations is case insensitive.
*
* @param rel
* @return
*/
public static boolean isIanaRel(LinkRelation rel) {
return
rel != null
&&
LINK_RELATIONS.stream().anyMatch(linkRelation -> linkRelation.value.equalsIgnoreCase(rel.value()));
return rel != null && LINK_RELATIONS.stream() //
.anyMatch(it -> it.value().equalsIgnoreCase(rel.value()));
}
/**
* Convert a string-based link relation to a {@link IanaLinkRelation}.
*
* Per RFC8288, parsing of link relations is case insensitive.
* Convert a string-based link relation to a {@link IanaLinkRelations}. Per RFC8288, parsing of link relations is case
* insensitive.
*
* @param rel as a string
* @return rel as a {@link IanaLinkRelation}
* @return rel as a {@link IanaLinkRelations}
*/
public static IanaLinkRelation parse(String rel) {
public static LinkRelation parse(String rel) {
return LINK_RELATIONS.stream()
.filter(linkRelation -> linkRelation.value().equalsIgnoreCase(rel))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(rel + " is not a valid IANA link relation!"));
return LINK_RELATIONS.stream() //
.filter(it -> it.value().equalsIgnoreCase(rel)) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException(rel + " is not a valid IANA link relation!"));
}
}

View File

@@ -33,10 +33,10 @@ public class IanaRels {
*
* @param rel the relation type to check
* @return
* @deprecated Migrate to {@link IanaLinkRelation#isIanaRel(String)}.
* @deprecated Migrate to {@link IanaLinkRelations#isIanaRel(String)}.
*/
@Deprecated
public static boolean isIanaRel(String rel) {
return IanaLinkRelation.isIanaRel(rel);
return IanaLinkRelations.isIanaRel(rel);
}
}

View File

@@ -41,7 +41,7 @@ import com.fasterxml.jackson.annotation.JsonInclude;
/**
* Value object for links.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
* @author Jens Schauder
@@ -50,7 +50,8 @@ import com.fasterxml.jackson.annotation.JsonInclude;
@JsonIgnoreProperties(value = "templated", ignoreUnknown = true)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@Getter
@EqualsAndHashCode(of = { "rel", "href", "hreflang", "media", "title", "type", "deprecation", "profile", "name", "affordances" })
@EqualsAndHashCode(
of = { "rel", "href", "hreflang", "media", "title", "type", "deprecation", "profile", "name", "affordances" })
public class Link implements Serializable {
private static final long serialVersionUID = -9037755944661782121L;
@@ -63,36 +64,31 @@ public class Link implements Serializable {
public static final String ATOM_NAMESPACE = "http://www.w3.org/2005/Atom";
/**
* @deprecated Use {@link IanaLinkRelation#SELF} instead.
* @deprecated Use {@link IanaLinkRelations#SELF} instead.
*/
@Deprecated
public static final String REL_SELF = IanaLinkRelation.SELF.value();
public static final @Deprecated LinkRelation REL_SELF = IanaLinkRelations.SELF;
/**
* @deprecated Use {@link IanaLinkRelation#FIRST} instead.
* @deprecated Use {@link IanaLinkRelations#FIRST} instead.
*/
@Deprecated
public static final String REL_FIRST = IanaLinkRelation.FIRST.value();
public static final @Deprecated LinkRelation REL_FIRST = IanaLinkRelations.FIRST;
/**
* @deprecated Use {@link IanaLinkRelation#PREV} instead.
* @deprecated Use {@link IanaLinkRelations#PREV} instead.
*/
@Deprecated
public static final String REL_PREVIOUS = IanaLinkRelation.PREV.value();
public static final @Deprecated LinkRelation REL_PREVIOUS = IanaLinkRelations.PREV;
/**
* @deprecated Use {@link IanaLinkRelation#NEXT} instead.
* @deprecated Use {@link IanaLinkRelations#NEXT} instead.
*/
@Deprecated
public static final String REL_NEXT = IanaLinkRelation.NEXT.value();
public static final @Deprecated LinkRelation REL_NEXT = IanaLinkRelations.NEXT;
/**
* @deprecated Use {@link IanaLinkRelation#LAST} instead.
* @deprecated Use {@link IanaLinkRelations#LAST} instead.
*/
@Deprecated
public static final String REL_LAST = IanaLinkRelation.LAST.value();
public static final @Deprecated LinkRelation REL_LAST = IanaLinkRelations.LAST;
private @Wither String rel;
private LinkRelation rel;
private @Wither String href;
private @Wither String hreflang;
private @Wither String media;
@@ -106,52 +102,74 @@ public class Link implements Serializable {
/**
* Creates a new link to the given URI with the self rel.
*
* @see IanaLinkRelation#SELF
*
* @see IanaLinkRelations#SELF
* @param href must not be {@literal null} or empty.
*/
public Link(String href) {
this(href, IanaLinkRelation.SELF.value());
this(href, IanaLinkRelations.SELF);
}
/**
* 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.
*/
public Link(String href, String rel) {
this(new UriTemplate(href), LinkRelation.of(rel));
}
/**
* 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.
*/
public Link(String href, LinkRelation rel) {
this(new UriTemplate(href), rel);
}
/**
* 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.
*/
public Link(UriTemplate template, String rel) {
Assert.notNull(template, "UriTemplate must not be null!");
Assert.hasText(rel, "Rel must not be null or empty!");
this.template = template;
this.href = template.toString();
this.rel = rel;
this.affordances = new ArrayList<>();
this(template, LinkRelation.of(rel));
}
public Link(String href, String rel, List<Affordance> affordances) {
/**
* 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.
*/
public Link(UriTemplate template, LinkRelation rel) {
this(template, rel, Collections.emptyList());
}
this(href, rel);
/**
* 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<Affordance> affordances) {
Assert.notNull(affordances, "affordances must not be null!");
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;
}
/**
* Empty constructor required by the marshalling framework.
* Empty constructor required by the marshaling framework.
*/
protected Link() {
this.affordances = new ArrayList<>();
@@ -159,7 +177,7 @@ public class Link implements Serializable {
/**
* Returns safe copy of {@link Affordance}s.
*
*
* @return
*/
public List<Affordance> getAffordances() {
@@ -168,11 +186,11 @@ public class Link implements Serializable {
/**
* Returns a {@link Link} pointing to the same URI but with the {@code self} relation.
*
*
* @return
*/
public Link withSelfRel() {
return withRel(IanaLinkRelation.SELF.value());
return withRel(IanaLinkRelations.SELF);
}
/**
@@ -194,7 +212,7 @@ public class Link implements Serializable {
/**
* Convenience method when chaining an existing {@link Link}.
*
*
* @param name
* @param httpMethod
* @param inputType
@@ -202,13 +220,14 @@ public class Link implements Serializable {
* @param outputType
* @return
*/
public Link andAffordance(String name, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
public Link andAffordance(String name, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
return andAffordance(new Affordance(name, this, httpMethod, inputType, queryMethodParameters, outputType));
}
/**
* Convenience method when chaining an existing {@link Link}. Defaults the name of the affordance to verb + classname, e.g.
* {@literal <httpMethod=HttpMethod.PUT, inputType=Employee>} produces {@literal <name=putEmployee>}.
* Convenience method when chaining an existing {@link Link}. Defaults the name of the affordance to verb + classname,
* e.g. {@literal <httpMethod=HttpMethod.PUT, inputType=Employee>} produces {@literal <name=putEmployee>}.
*
* @param httpMethod
* @param inputType
@@ -216,13 +235,15 @@ public class Link implements Serializable {
* @param outputType
* @return
*/
public Link andAffordance(HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
return andAffordance(httpMethod.toString().toLowerCase() + inputType.resolve().getSimpleName(), httpMethod, inputType, queryMethodParameters, outputType);
public Link andAffordance(HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters,
ResolvableType outputType) {
return andAffordance(httpMethod.toString().toLowerCase() + inputType.resolve().getSimpleName(), httpMethod,
inputType, queryMethodParameters, outputType);
}
/**
* Convenience method when chaining an existing {@link Link}. Defaults the name of the affordance to verb + classname, e.g.
* {@literal <httpMethod=HttpMethod.PUT, inputType=Employee>} produces {@literal <name=putEmployee>}.
* Convenience method when chaining an existing {@link Link}. Defaults the name of the affordance to verb + classname,
* e.g. {@literal <httpMethod=HttpMethod.PUT, inputType=Employee>} produces {@literal <name=putEmployee>}.
*
* @param httpMethod
* @param inputType
@@ -230,13 +251,15 @@ public class Link implements Serializable {
* @param outputType
* @return
*/
public Link andAffordance(HttpMethod httpMethod, Class<?> inputType, List<QueryParameter> queryMethodParameters, Class<?> outputType) {
return andAffordance(httpMethod, ResolvableType.forClass(inputType), queryMethodParameters, ResolvableType.forClass(outputType));
public Link andAffordance(HttpMethod httpMethod, Class<?> inputType, List<QueryParameter> queryMethodParameters,
Class<?> outputType) {
return andAffordance(httpMethod, ResolvableType.forClass(inputType), queryMethodParameters,
ResolvableType.forClass(outputType));
}
/**
* Create new {@link Link} with additional {@link Affordance}s.
*
*
* @param affordances must not be {@literal null}.
* @return
*/
@@ -251,7 +274,7 @@ public class Link implements Serializable {
/**
* Creats a new {@link Link} with the given {@link Affordance}s.
*
*
* @param affordances must not be {@literal null}.
* @return
*/
@@ -263,7 +286,7 @@ public class Link implements Serializable {
/**
* Returns the variable names contained in the template.
*
*
* @return
*/
@JsonIgnore
@@ -273,7 +296,7 @@ public class Link implements Serializable {
/**
* Returns all {@link TemplateVariables} contained in the {@link Link}.
*
*
* @return
*/
@JsonIgnore
@@ -283,7 +306,7 @@ public class Link implements Serializable {
/**
* Returns whether or not the link is templated.
*
*
* @return
*/
public boolean isTemplated() {
@@ -292,7 +315,7 @@ public class Link implements Serializable {
/**
* Turns the current template into a {@link Link} by expanding it using the given parameters.
*
*
* @param arguments
* @return
*/
@@ -302,7 +325,7 @@ public class Link implements Serializable {
/**
* Turns the current template into a {@link Link} by expanding it using the given parameters.
*
*
* @param arguments must not be {@literal null}.
* @return
*/
@@ -310,9 +333,32 @@ public class Link implements Serializable {
return new Link(getUriTemplate().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
*/
@@ -320,7 +366,20 @@ public class Link implements Serializable {
Assert.hasText(rel, "Link relation must not be null or empty!");
return this.rel.equals(rel);
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);
}
private UriTemplate getUriTemplate() {
@@ -339,7 +398,7 @@ public class Link implements Serializable {
@Override
public String toString() {
String linkString = String.format("<%s>;rel=\"%s\"", href, rel);
String linkString = String.format("<%s>;rel=\"%s\"", href, rel.value());
if (hreflang != null) {
linkString += ";hreflang=\"" + hreflang + "\"";
@@ -375,7 +434,7 @@ public class Link implements Serializable {
/**
* Factory method to easily create {@link Link} instances from RFC-5988 compatible {@link String} representations of a
* link. Will return {@literal null} if input {@link String} is either empty or {@literal null}.
*
*
* @param element an RFC-5899 compatible representation of a link.
* @throws IllegalArgumentException if a non-empty {@link String} was given that does not adhere to RFC-5899.
* @throws IllegalArgumentException if no {@code rel} attribute could be found.
@@ -436,7 +495,7 @@ public class Link implements Serializable {
/**
* Parses the links attributes from the given source {@link String}.
*
*
* @param source
* @return
*/

View File

@@ -19,7 +19,7 @@ import java.net.URI;
/**
* Builder to ease building {@link Link} instances.
*
*
* @author Ricardo Gladwell
*/
public interface LinkBuilder {
@@ -27,7 +27,7 @@ public interface LinkBuilder {
/**
* Adds the given object's {@link String} representation as sub-resource to the current URI. Will unwrap
* {@link Identifiable}s to their id value (see {@link Identifiable#getId()}).
*
*
* @param object
* @return
*/
@@ -36,7 +36,7 @@ public interface LinkBuilder {
/**
* Adds the given {@link Identifiable}'s id as sub-resource. Will simply return the {@link LinkBuilder} as is if the
* given entity is {@literal null}.
*
*
* @param identifiable
* @return
*/
@@ -44,23 +44,33 @@ public interface LinkBuilder {
/**
* Creates a URI of the link built by the current builder instance.
*
*
* @return
*/
URI toUri();
/**
* Creates the {@link Link} built by the current builder instance with the given rel.
*
* Creates the {@link Link} built by the current builder instance with the given link relation.
*
* @param rel must not be {@literal null}.
* @return
*/
default Link withRel(String rel) {
return withRel(LinkRelation.of(rel));
}
/**
* Creates the {@link Link} built by the current builder instance with the given {@link LinkRelation}.
*
* @param rel must not be {@literal null} or empty.
* @return
*/
Link withRel(String rel);
Link withRel(LinkRelation rel);
/**
* Creates the {@link Link} built by the current builder instance with the default self rel.
*
* @see IanaLinkRelation#SELF
* Creates the {@link Link} built by the current builder instance with the default self link relation.
*
* @see IanaLinkRelations#SELF
* @return
*/
Link withSelfRel();

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012 the original author or authors.
* Copyright 2012-2019 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.
@@ -16,51 +16,88 @@
package org.springframework.hateoas;
import java.io.InputStream;
import java.util.List;
import java.util.Optional;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.Plugin;
/**
* Interface to allow discovering links by relation type from some source.
*
*
* @author Oliver Gierke
*/
public interface LinkDiscoverer extends Plugin<MediaType> {
/**
* Finds a single link with the given relation type in the given {@link String} representation.
*
*
* @param rel must not be {@literal null} or empty.
* @param representation must not be {@literal null} or empty.
* @param representation must not be {@literal null}.
* @return the first link with the given relation type found, or {@literal null} if none was found.
*/
Link findLinkWithRel(String rel, String representation);
default Optional<Link> findLinkWithRel(String rel, String representation) {
return findLinkWithRel(LinkRelation.of(rel), representation);
}
Optional<Link> findLinkWithRel(LinkRelation rel, String representation);
/**
* Finds a single link with the given relation type in the given {@link InputStream} representation.
*
*
* @param rel must not be {@literal null} or empty.
* @param representation must not be {@literal null} or empty.
* @param representation must not be {@literal null}.
* @return the first link with the given relation type found, or {@literal null} if none was found.
*/
Link findLinkWithRel(String rel, InputStream representation);
default Optional<Link> findLinkWithRel(String rel, InputStream representation) {
return findLinkWithRel(LinkRelation.of(rel), representation);
}
/**
* Returns all links with the given relation type found in the given {@link String} representation.
*
* @param rel must not be {@literal null} or empty.
* @param representation must not be {@literal null} or empty.
* @return
* Finds a single link with the given {@link LinkRelation} in the given {@link InputStream} representation.
*
* @param rel must not be {@literal null}.
* @param representation must not be {@literal null}.
* @return the first link with the given relation type found, or {@literal null} if none was found.
*/
List<Link> findLinksWithRel(String rel, String representation);
Optional<Link> findLinkWithRel(LinkRelation rel, InputStream representation);
/**
* Returns all links with the given relation type found in the given {@link InputStream} representation.
*
* Returns all links with the given link relation found in the given {@link String} representation.
*
* @param rel must not be {@literal null} or empty.
* @param representation must not be {@literal null} or empty.
* @return
* @param representation must not be {@literal null}.
* @return will never be {@literal null}.
*/
List<Link> findLinksWithRel(String rel, InputStream representation);
default Links findLinksWithRel(String rel, String representation) {
return findLinksWithRel(LinkRelation.of(rel), representation);
}
/**
* Returns all links with the given {@link LinkRelation} found in the given {@link String} representation.
*
* @param rel must not be {@literal null}.
* @param representation must not be {@literal null}.
* @return will never be {@literal null}.
*/
Links findLinksWithRel(LinkRelation rel, String representation);
/**
* Returns all links with the given link relation found in the given {@link InputStream} representation.
*
* @param rel must not be {@literal null} or empty.
* @param representation must not be {@literal null}.
* @return will never be {@literal null}.
*/
default Links findLinksWithRel(String rel, InputStream representation) {
return findLinksWithRel(LinkRelation.of(rel), representation);
}
/**
* Returns all links with the given {@link LinkRelation} found in the given {@link InputStream} representation.
*
* @param rel must not be {@literal null}.
* @param representation must not be {@literal null}.
* @return will never be {@literal null}.
*/
Links findLinksWithRel(LinkRelation rel, InputStream representation);
}

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.hateoas;
import java.util.Optional;
import org.springframework.http.MediaType;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
@@ -22,7 +24,7 @@ import org.springframework.util.Assert;
/**
* Value object to wrap a {@link PluginRegistry} for {@link LinkDiscoverer} so that it's easier to inject them into
* clients wanting to lookup a {@link LinkDiscoverer} for a given {@link MediaTypes}.
*
*
* @author Oliver Gierke
*/
public class LinkDiscoverers {
@@ -31,7 +33,7 @@ public class LinkDiscoverers {
/**
* Creates a new {@link LinkDiscoverers} instance with the given {@link PluginRegistry}.
*
*
* @param discoverers must not be {@literal null}.
*/
public LinkDiscoverers(PluginRegistry<LinkDiscoverer, MediaType> discoverers) {
@@ -42,21 +44,41 @@ public class LinkDiscoverers {
/**
* Returns the {@link LinkDiscoverer} suitable for the given {@link MediaType}.
*
*
* @param mediaType
* @return will never be {@literal null}.
*/
public Optional<LinkDiscoverer> getLinkDiscovererFor(MediaType mediaType) {
return discoverers.getPluginFor(mediaType);
}
/**
* Returns the {@link LinkDiscoverer} suitable for the given media type.
*
* @param mediaType
* @return
*/
public LinkDiscoverer getLinkDiscovererFor(MediaType mediaType) {
public Optional<LinkDiscoverer> getLinkDiscovererFor(String mediaType) {
return getLinkDiscovererFor(MediaType.valueOf(mediaType));
}
/**
* Returns the {@link LinkDiscoverer} suitable for the given {@link MediaType}.
*
* @param mediaType
* @return will never be {@literal null}.
*/
public LinkDiscoverer getRequiredLinkDiscovererFor(MediaType mediaType) {
return discoverers.getRequiredPluginFor(mediaType);
}
/**
* Returns the {@link LinkDiscoverer} suitable for the given media type.
*
*
* @param mediaType
* @return
*/
public LinkDiscoverer getLinkDiscovererFor(String mediaType) {
return getLinkDiscovererFor(MediaType.valueOf(mediaType));
public LinkDiscoverer getRequiredLinkDiscovererFor(String mediaType) {
return getRequiredLinkDiscovererFor(MediaType.valueOf(mediaType));
}
}

View File

@@ -15,10 +15,19 @@
*/
package org.springframework.hateoas;
import java.util.Arrays;
import java.util.stream.Collectors;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Interface for defining link relations. Can be used for implementing spec-based link relations as well as custom ones.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
* @since 1.0
*/
public interface LinkRelation {
@@ -26,5 +35,44 @@ public interface LinkRelation {
/**
* Return the link relation's value.
*/
@JsonValue
String value();
/**
* Creates a new {@link LinkRelation}.
*
* @param relation must not be {@literal null} or empty.
* @return
*/
@JsonCreator
static LinkRelation of(String relation) {
return StringLinkRelation.of(relation);
}
/**
* Creates a new {@link Iterable} of {@link LinkRelation} for each of the given {@link String}s.
*
* @param others must not be {@literal null}.
* @return
*/
static Iterable<LinkRelation> manyOf(String... others) {
return Arrays.stream(others) //
.map(LinkRelation::of) //
.collect(Collectors.toList());
}
/**
* Returns whether the given {@link LinkRelation} is logically the same as the current one, independent of
* implementation, i.e. whether the plain {@link String} values match.
*
* @param relation must not be {@literal null}.
* @return
*/
default boolean isSameAs(LinkRelation relation) {
Assert.notNull(relation, "LinkRelation must not be null!");
return this.value().equals(relation.value());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2013-2017 the original author or authors.
* Copyright 2013-2019 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.
@@ -21,12 +21,19 @@ import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Matcher;
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.util.Assert;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Value object to represent a list of {@link Link}s.
*
@@ -35,75 +42,51 @@ import org.springframework.util.StringUtils;
*/
public class Links implements Iterable<Link> {
public static final Links NONE = new Links(Collections.emptyList());
private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("(<[^>]*>(;\\s*\\w+=\"[^\"]*\")+)");
static final Links NO_LINKS = new Links(Collections.emptyList());
private final List<Link> links;
/**
* Creates a new {@link Links} instance from the given {@link Link}s.
*
* @param links
*/
public Links(List<Link> links) {
this.links = links == null ? Collections.emptyList() : Collections.unmodifiableList(links);
private Links(Iterable<Link> links) {
Assert.notNull(links, "Links must not be null!");
this.links = StreamSupport.stream(links.spliterator(), false) //
.collect(Collectors.toList());
}
/**
* Creates a new {@link Links} instance from the given {@link Link}s.
*
* @param links
*/
public Links(Link... links) {
private Links(Link... links) {
this(Arrays.asList(links));
}
/**
* Returns the {@link Link} with the given rel.
* Creates a new {@link Links} instance from the given {@link Link}s.
*
* @param rel the relation type to lookup a link for.
* @return the link with the given rel or {@literal Optional#empty()} if none found.
* @param links
*/
public Optional<Link> getLink(String rel) {
return links.stream() //
.filter(link -> link.getRel().equals(rel)).findFirst();
public static Links of(Link... links) {
return new Links(links);
}
/**
* Returns the {@link Link} with the given relation.
* Creates a new {@link Links} instance from the given {@link Link}s.
*
* @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
* @param links
*/
public Link getRequiredLink(String rel) {
return getLink(rel) //
.orElseThrow(() -> new IllegalArgumentException(String.format("Couldn't find link with rel '%s'!", rel)));
public static Links of(Iterable<Link> links) {
return new Links(links);
}
/**
* Returns all {@link Links} with the given relation type.
* Creates a {@link Links} instance from the given RFC5988-compatible link format.
*
* @return the links
* @param source a comma separated list of {@link Link} representations.
* @return the {@link Links} represented by the given {@link String}.
* @deprecated use {@link #parse(String)} instead
*/
public List<Link> getLinks(String rel) {
return links.stream() //
.filter(link -> link.getRel().endsWith(rel)).collect(Collectors.toList());
}
/**
* Returns whether the {@link Links} container contains a {@link Link} with the given rel.
*
* @param rel
* @return
*/
public boolean hasLink(String rel) {
return getLink(rel).isPresent();
@Deprecated
public static Links valueOf(String source) {
return parse(source);
}
/**
@@ -112,10 +95,10 @@ public class Links implements Iterable<Link> {
* @param source a comma separated list of {@link Link} representations.
* @return the {@link Links} represented by the given {@link String}.
*/
public static Links valueOf(String source) {
public static Links parse(String source) {
if (!StringUtils.hasText(source)) {
return NO_LINKS;
return NONE;
}
Matcher matcher = LINK_HEADER_PATTERN.matcher(source);
@@ -133,6 +116,194 @@ public class Links implements Iterable<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));
}
/**
* 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<Link> links) {
List<Link> newLinks = new ArrayList<>(this.links);
links.forEach(newLinks::add);
return Links.of(newLinks);
}
/**
* 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(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(Iterable<Link> 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, Iterable<Link> links) {
Assert.notNull(mode, "MergeMode must not be null!");
Assert.notNull(links, "Links must not be null!");
List<Link> 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<Link> 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<Link> 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.
*
@@ -142,6 +313,86 @@ public class Links implements Iterable<Link> {
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<Link> stream() {
return this.links.stream();
}
/**
* Returns the current {@link Links} as {@link List}.
*
* @return
*/
@JsonValue
public List<Link> 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<Link> 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<Link> 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<Link, ?, Links> collector() {
return Collectors.collectingAndThen(Collectors.toList(), Links::of);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
@@ -188,4 +439,38 @@ public class Links implements Iterable<Link> {
return result;
}
private List<Link> allWithoutRels(Iterable<Link> links) {
Set<LinkRelation> 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 static 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;
}
}

View File

@@ -27,7 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* DTO to implement binding response representations of pageable collections.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -46,7 +46,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Creates a new {@link PagedResources} from the given content, {@link PageMetadata} and {@link Link}s (optional).
*
*
* @param content must not be {@literal null}.
* @param metadata
* @param links
@@ -57,7 +57,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Creates a new {@link PagedResources} from the given content {@link PageMetadata} and {@link Link}s.
*
*
* @param content must not be {@literal null}.
* @param metadata
* @param links
@@ -69,7 +69,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Returns the pagination metadata.
*
*
* @return the metadata
*/
@JsonProperty("page")
@@ -79,7 +79,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Factory method to easily create a {@link PagedResources} instance from a set of entities and pagination metadata.
*
*
* @param content must not be {@literal null}.
* @param metadata
* @return
@@ -99,25 +99,25 @@ public class PagedResources<T> extends Resources<T> {
/**
* Returns the Link pointing to the next page (if set).
*
*
* @return
*/
@JsonIgnore
public Optional<Link> getNextLink() {
return getLink(IanaLinkRelation.NEXT.value());
return getLink(IanaLinkRelations.NEXT);
}
/**
* Returns the Link pointing to the previous page (if set).
*
*
* @return
*/
@JsonIgnore
public Optional<Link> getPreviousLink() {
return getLink(IanaLinkRelation.PREV.value());
return getLink(IanaLinkRelations.PREV);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.ResourceSupport#toString()
*/
@@ -126,7 +126,7 @@ public class PagedResources<T> extends Resources<T> {
return String.format("PagedResource { content: %s, metadata: %s, links: %s }", getContent(), metadata, getLinks());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Resources#equals(java.lang.Object)
*/
@@ -147,7 +147,7 @@ public class PagedResources<T> extends Resources<T> {
return metadataEquals && super.equals(obj);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.Resources#hashCode()
*/
@@ -161,7 +161,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Value object for pagination metadata.
*
*
* @author Oliver Gierke
*/
public static class PageMetadata {
@@ -177,7 +177,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Creates a new {@link PageMetadata} from the given size, number, total elements and total pages.
*
*
* @param size
* @param number zero-indexed, must be less than totalPages
* @param totalElements
@@ -198,7 +198,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Creates a new {@link PageMetadata} from the given size, number and total elements.
*
*
* @param size the size of the page
* @param number the number of the page
* @param totalElements the total number of elements available
@@ -209,7 +209,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Returns the requested size of the page.
*
*
* @return the size a positive long.
*/
public long getSize() {
@@ -218,7 +218,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Returns the total number of elements available.
*
*
* @return the totalElements a positive long.
*/
public long getTotalElements() {
@@ -227,7 +227,7 @@ public class PagedResources<T> extends Resources<T> {
/**
* Returns how many pages are available in total.
*
*
* @return the totalPages a positive long.
*/
public long getTotalPages() {
@@ -236,14 +236,14 @@ public class PagedResources<T> extends Resources<T> {
/**
* Returns the number of the current page.
*
*
* @return the number a positive long.
*/
public long getNumber() {
return number;
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@@ -253,7 +253,7 @@ public class PagedResources<T> extends Resources<T> {
totalElements, size);
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@@ -274,7 +274,7 @@ public class PagedResources<T> extends Resources<T> {
&& this.totalPages == that.totalPages;
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/

View File

@@ -19,24 +19,24 @@ import org.springframework.plugin.core.Plugin;
/**
* API to provide relation types for collections and items of the given type.
*
*
* @author Oliver Gierke
*/
public interface RelProvider extends Plugin<Class<?>> {
/**
* Returns the relation type to be used to point to an item resource of the given type.
*
*
* @param type must not be {@literal null}.
* @return
*/
String getItemResourceRelFor(Class<?> type);
LinkRelation getItemResourceRelFor(Class<?> type);
/**
* Returns the relation type to be used to point to a collection resource of the given type.
*
*
* @param type must not be {@literal null}.
* @return
*/
String getCollectionResourceRelFor(Class<?> type);
LinkRelation getCollectionResourceRelFor(Class<?> type);
}

View File

@@ -28,7 +28,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Base class for DTOs to collect links.
*
*
* @author Oliver Gierke
* @author Johhny Lim
* @author Greg Turnquist
@@ -42,16 +42,16 @@ public class ResourceSupport implements Identifiable<Link> {
}
/**
* Returns the {@link Link} with a rel of {@link IanaLinkRelation#SELF}.
* Returns the {@link Link} with a rel of {@link IanaLinkRelations#SELF}.
*/
@JsonIgnore
public Optional<Link> getId() {
return getLink(IanaLinkRelation.SELF.value());
return getLink(IanaLinkRelations.SELF);
}
/**
* Adds the given link to the resource.
*
*
* @param link
*/
public void add(Link link) {
@@ -63,7 +63,7 @@ public class ResourceSupport implements Identifiable<Link> {
/**
* Adds all given {@link Link}s to the resource.
*
*
* @param links
*/
public void add(Iterable<Link> links) {
@@ -87,7 +87,7 @@ public class ResourceSupport implements Identifiable<Link> {
/**
* Returns whether the resource contains {@link Link}s at all.
*
*
* @return
*/
public boolean hasLinks() {
@@ -96,7 +96,7 @@ public class ResourceSupport implements Identifiable<Link> {
/**
* Returns whether the resource contains a {@link Link} with the given rel.
*
*
* @param rel
* @return
*/
@@ -104,14 +104,18 @@ public class ResourceSupport implements Identifiable<Link> {
return getLink(rel).isPresent();
}
public boolean hasLink(LinkRelation rel) {
return hasLink(rel.value());
}
/**
* Returns all {@link Link}s contained in this resource.
*
*
* @return
*/
@JsonProperty("links")
public List<Link> getLinks() {
return links;
public Links getLinks() {
return Links.of(links);
}
/**
@@ -123,20 +127,24 @@ public class ResourceSupport implements Identifiable<Link> {
/**
* Returns the link with the given rel.
*
*
* @param rel
* @return the link with the given rel or {@link Optional#empty()} if none found.
*/
public Optional<Link> getLink(String rel) {
return getLink(LinkRelation.of(rel));
}
public Optional<Link> getLink(LinkRelation rel) {
return links.stream() //
.filter(link -> link.hasRel(rel)) //
.filter(it -> it.hasRel(rel)) //
.findFirst();
}
/**
* Returns the link with the given rel.
*
*
* @param rel
* @return the link with the given rel.
* @throws IllegalArgumentException in case no link with the given rel can be found.
@@ -147,6 +155,10 @@ public class ResourceSupport implements Identifiable<Link> {
.orElseThrow(() -> new IllegalArgumentException(String.format("No link with rel %s found!", rel)));
}
public Link getRequiredLink(LinkRelation rel) {
return getRequiredLink(rel.value());
}
/**
* Returns all {@link Link}s with the given relation type.
*
@@ -159,7 +171,11 @@ public class ResourceSupport implements Identifiable<Link> {
.collect(Collectors.toList());
}
/*
public List<Link> getLinks(LinkRelation rel) {
return getLinks(rel.value());
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@@ -168,7 +184,7 @@ public class ResourceSupport implements Identifiable<Link> {
return String.format("links: %s", links.toString());
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@@ -188,7 +204,7 @@ public class ResourceSupport implements Identifiable<Link> {
return this.links.equals(that.links);
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#hashCode()
*/

View File

@@ -27,7 +27,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
/**
* General helper to easily create a wrapper for a collection of entities.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -44,7 +44,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
/**
* Creates a {@link Resources} instance with the given content and {@link Link}s (optional).
*
*
* @param content must not be {@literal null}.
* @param links the links to be added to the {@link Resources}.
*/
@@ -54,7 +54,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
/**
* Creates a {@link Resources} instance with the given content and {@link Link}s.
*
*
* @param content must not be {@literal null}.
* @param links the links to be added to the {@link Resources}.
*/
@@ -72,7 +72,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
/**
* Creates a new {@link Resources} instance by wrapping the given domain class instances into a {@link Resource}.
*
*
* @param content must not be {@literal null}.
* @return
*/
@@ -80,6 +80,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
public static <T extends Resource<S>, S> Resources<T> wrap(Iterable<S> content) {
Assert.notNull(content, "Content must not be null!");
ArrayList<T> resources = new ArrayList<>();
for (S element : content) {
@@ -91,7 +92,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
/**
* Returns the underlying elements.
*
*
* @return the content will never be {@literal null}.
*/
@JsonProperty("content")
@@ -99,7 +100,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
return Collections.unmodifiableCollection(content);
}
/*
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@@ -117,7 +118,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
return String.format("Resources { content: %s, %s }", getContent(), super.toString());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.ResourceSupport#equals(java.lang.Object)
*/
@@ -138,7 +139,7 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
return contentEqual && super.equals(obj);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.ResourceSupport#hashCode()
*/

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2019 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
*
* 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 org.springframework.hateoas;
import lombok.AccessLevel;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Simple value type for a {@link String} based {@link LinkRelation}.
*
* @author Oliver Drotbohm
*/
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class StringLinkRelation implements LinkRelation, Serializable {
private static final long serialVersionUID = -3904935345545567957L;
private static final Map<String, StringLinkRelation> CACHE = new ConcurrentHashMap<String, StringLinkRelation>(256);
@NonNull String relation;
/**
* Returns a (potentially cached) {@link LinkRelation} for the given value.
*
* @param relation must not be {@literal null} or empty.
* @return
*/
@JsonCreator
public static StringLinkRelation of(String relation) {
Assert.hasText(relation, "Relation must not be null or empty!");
return CACHE.computeIfAbsent(relation, it -> new StringLinkRelation(it));
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkRelation#value()
*/
@JsonValue
@Override
public String value() {
return relation;
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return value();
}
}

View File

@@ -22,8 +22,6 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@@ -36,7 +34,7 @@ import org.springframework.web.util.UriComponentsBuilder;
/**
* Custom URI template to support qualified URI template variables.
*
*
* @author Oliver Gierke
* @author JamesE Richardson
* @see http://tools.ietf.org/html/rfc6570
@@ -52,7 +50,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Creates a new {@link UriTemplate} using the given template string.
*
*
* @param template must not be {@literal null} or empty.
*/
public UriTemplate(String template) {
@@ -71,7 +69,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
String[] names = matcher.group(2).split(",");
for (String name : names) {
TemplateVariable variable;
if (name.endsWith(VariableType.COMPOSITE_PARAM.toString())) {
@@ -94,7 +92,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Creates a new {@link UriTemplate} from the given base URI and {@link TemplateVariables}.
*
*
* @param baseUri must not be {@literal null} or empty.
* @param variables defaults to {@link TemplateVariables#NONE}.
*/
@@ -108,7 +106,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Creates a new {@link UriTemplate} with the current {@link TemplateVariable}s augmented with the given ones.
*
*
* @param variables can be {@literal null}.
* @return will never be {@literal null}.
*/
@@ -142,7 +140,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* 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}.
@@ -153,7 +151,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Returns whether the given candidate is a URI template.
*
*
* @param candidate
* @return
*/
@@ -168,7 +166,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Returns the {@link TemplateVariable}s discovered.
*
*
* @return
*/
public List<TemplateVariable> getVariables() {
@@ -177,7 +175,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Returns the names of the variables discovered.
*
*
* @return
*/
public List<String> getVariableNames() {
@@ -189,7 +187,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* 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)
@@ -215,7 +213,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Expands the {@link UriTemplate} using the given parameters.
*
*
* @param parameters must not be {@literal null}.
* @return
*/
@@ -237,7 +235,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
return builder.build().toUri();
}
/*
/*
* (non-Javadoc)
* @see java.lang.Iterable#iterator()
*/
@@ -246,7 +244,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
return this.variables.iterator();
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@@ -268,7 +266,7 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
/**
* Appends the value for the given {@link TemplateVariable} to the given {@link UriComponentsBuilder}.
*
*
* @param builder must not be {@literal null}.
* @param variable must not be {@literal null}.
* @param value can be {@literal null}.
@@ -311,17 +309,20 @@ public class UriTemplate implements Iterable<TemplateVariable>, Serializable {
* @param value
* @see https://tools.ietf.org/html/rfc6570#section-2.4.2
*/
@SuppressWarnings("unchecked")
private static void appendComposite(UriComponentsBuilder builder, String name, Object value) {
if (value instanceof Iterable) {
for (Object valuePart : (Iterable<?>) value) {
builder.queryParam(name, valuePart);
}
((Iterable<?>) value).forEach(it -> builder.queryParam(name, it));
} else if (value instanceof Map) {
for (Entry<Object, Object> queryParam : (Set<Entry<Object, Object>>) ((Map) value).entrySet()) {
builder.queryParam(queryParam.getKey().toString(), queryParam.getValue());
}
((Map<Object, Object>) value).entrySet() //
.forEach(it -> builder.queryParam(it.getKey().toString(), it.getValue()));
} else {
builder.queryParam(name, value);
}
}

View File

@@ -29,7 +29,7 @@ import com.fasterxml.jackson.annotation.JsonValue;
/**
* A representation model class to be rendered as specified for the media type {@code application/vnd.error}.
*
*
* @see https://github.com/blongden/vnd.error
* @author Oliver Gierke
* @author Greg Turnquist
@@ -41,7 +41,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Creates a new {@link VndErrors} instance containing a single {@link VndError} with the given logref, message and
* optional {@link Link}s.
*
*
* @param logref must not be {@literal null} or empty.
* @param message must not be {@literal null} or empty.
* @param links
@@ -52,7 +52,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Creates a new {@link VndErrors} wrapper for at least one {@link VndError}.
*
*
* @param errors must not be {@literal null}.
*/
public VndErrors(VndError error, VndError... errors) {
@@ -66,7 +66,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Creates a new {@link VndErrors} wrapper for the given {@link VndErrors}.
*
*
* @param errors must not be {@literal null} or empty.
*/
@JsonCreator
@@ -86,7 +86,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Adds an additional {@link VndError} to the wrapper.
*
*
* @param error
*/
public VndErrors add(VndError error) {
@@ -96,7 +96,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Dummy method to allow {@link JsonValue} to be configured.
*
*
* @return the vndErrors
*/
@JsonValue
@@ -113,7 +113,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
return this.vndErrors.iterator();
}
/*
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@@ -152,7 +152,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* A single {@link VndError}.
*
*
* @author Oliver Gierke
*/
public static class VndError extends ResourceSupport {
@@ -162,7 +162,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Creates a new {@link VndError} with the given logref, a message as well as some {@link Link}s.
*
*
* @param logref must not be {@literal null} or empty.
* @param message must not be {@literal null} or empty.
* @param links
@@ -188,7 +188,7 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Returns the logref of the error.
*
*
* @return the logref
*/
public String getLogref() {
@@ -197,21 +197,20 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
/**
* Returns the message of the error.
*
*
* @return the message
*/
public String getMessage() {
return message;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.ResourceSupport#toString()
*/
@Override
public String toString() {
return String.format("VndError[logref: %s, message: %s, links: [%s]]", logref, message,
StringUtils.collectionToCommaDelimitedString(getLinks()));
return String.format("VndError[logref: %s, message: %s, links: [%s]]", logref, message, getLinks().toString());
}
/*

View File

@@ -15,6 +15,8 @@
*/
package org.springframework.hateoas.client;
import java.util.Optional;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkDiscoverers;
@@ -25,7 +27,7 @@ import com.jayway.jsonpath.JsonPath;
/**
* Helper class to find {@link Link} instances in representations.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
* @since 0.11
@@ -34,7 +36,7 @@ class Rels {
/**
* Creates a new {@link Rel} for the given relation name and {@link LinkDiscoverers}.
*
*
* @param rel must not be {@literal null} or empty.
* @param discoverers must not be {@literal null}.
* @return
@@ -55,17 +57,17 @@ class Rels {
/**
* Returns the link contained in the given representation of the given {@link MediaType}.
*
*
* @param representation
* @param mediaType
* @return
*/
Link findInResponse(String representation, MediaType mediaType);
Optional<Link> findInResponse(String representation, MediaType mediaType);
}
/**
* {@link Rel} to using a {@link LinkDiscoverer} based on the given {@link MediaType}.
*
*
* @author Oliver Gierke
*/
private static class LinkDiscovererRel implements Rel {
@@ -75,7 +77,7 @@ class Rels {
/**
* Creates a new {@link LinkDiscovererRel} for the given relation name and {@link LinkDiscoverers}.
*
*
* @param rel must not be {@literal null} or empty.
* @param discoverers must not be {@literal null}.
*/
@@ -88,21 +90,16 @@ class Rels {
this.discoverers = discoverers;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.client.Rels.Rel#findInResponse(java.lang.String, org.springframework.http.MediaType)
*/
@Override
public Link findInResponse(String response, MediaType mediaType) {
public Optional<Link> findInResponse(String response, MediaType mediaType) {
LinkDiscoverer discoverer = discoverers.getLinkDiscovererFor(mediaType);
if (discoverer == null) {
throw new IllegalStateException(String.format("Did not find LinkDiscoverer supporting media type %s!",
mediaType));
}
return discoverer.findLinkWithRel(rel, response);
return discoverers //
.getRequiredLinkDiscovererFor(mediaType) //
.findLinkWithRel(rel, response);
}
/*
@@ -118,7 +115,7 @@ class Rels {
/**
* A relation that's being looked up by a JSONPath expression.
*
*
* @author Oliver Gierke
*/
private static class JsonPathRel implements Rel {
@@ -128,7 +125,7 @@ class Rels {
/**
* Creates a new {@link JsonPathRel} for the given JSON path.
*
*
* @param jsonPath must not be {@literal null} or empty.
*/
private JsonPathRel(String jsonPath) {
@@ -141,13 +138,13 @@ class Rels {
this.rel = lastSegment.contains("[") ? lastSegment.substring(0, lastSegment.indexOf("[")) : lastSegment;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.client.Rels.Rel#findInResponse(java.lang.String, org.springframework.http.MediaType)
*/
@Override
public Link findInResponse(String representation, MediaType mediaType) {
return new Link(JsonPath.read(representation, jsonPath).toString(), rel);
public Optional<Link> findInResponse(String representation, MediaType mediaType) {
return Optional.of(new Link(JsonPath.read(representation, jsonPath).toString(), rel));
}
}
}

View File

@@ -453,14 +453,11 @@ public class Traverson {
String responseBody = responseEntity.getBody();
Hop thisHop = rels.next();
Rel rel = Rels.getRelFor(thisHop.getRel(), discoverers);
Link link = rel.findInResponse(responseBody, contentType);
if (link == null) {
throw new IllegalStateException(
String.format("Expected to find link with rel '%s' in response %s!", rel, responseBody));
}
Link link = rel.findInResponse(responseBody, contentType) //
.orElseThrow(() -> new IllegalStateException(
String.format("Expected to find link with rel '%s' in response %s!", rel, responseBody)));
/*
* Don't expand if the parameters are empty

View File

@@ -19,9 +19,13 @@ import lombok.AccessLevel;
import lombok.Value;
import lombok.experimental.Wither;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.Links.MergeMode;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -39,39 +43,62 @@ class CollectionJson<T> {
private String version;
private String href;
@JsonInclude(Include.NON_EMPTY)
private List<Link> links;
@JsonInclude(Include.NON_EMPTY)
private List<CollectionJsonItem<T>> items;
@JsonInclude(Include.NON_EMPTY)
private List<CollectionJsonQuery> queries;
@JsonInclude(Include.NON_NULL)
private CollectionJsonTemplate template;
@JsonInclude(Include.NON_NULL)
private CollectionJsonError error;
private @JsonInclude(Include.NON_EMPTY) Links links;
private @JsonInclude(Include.NON_EMPTY) List<CollectionJsonItem<T>> items;
private @JsonInclude(Include.NON_EMPTY) List<CollectionJsonQuery> queries;
private @JsonInclude(Include.NON_NULL) CollectionJsonTemplate template;
private @JsonInclude(Include.NON_NULL) CollectionJsonError error;
@JsonCreator
CollectionJson(@JsonProperty("version") String version, @JsonProperty("href") String href,
@JsonProperty("links") List<Link> links, @JsonProperty("items") List<CollectionJsonItem<T>> items,
@JsonProperty("queries") List<CollectionJsonQuery> queries,
@JsonProperty("template") CollectionJsonTemplate template,
@JsonProperty("error") CollectionJsonError error) {
CollectionJson(@JsonProperty("version") String version, //
@JsonProperty("href") String href, //
@JsonProperty("links") Links links, //
@JsonProperty("items") List<CollectionJsonItem<T>> items, //
@JsonProperty("queries") List<CollectionJsonQuery> queries, //
@JsonProperty("template") CollectionJsonTemplate template, //
@JsonProperty("error") CollectionJsonError error) {
this.version = version;
this.href = href;
this.links = links;
this.items = items;
this.links = links == null ? Links.NONE : links;
this.items = items == null ? Collections.emptyList() : items;
this.queries = queries;
this.template = template;
this.error = error;
}
CollectionJson() {
this("1.0", null, null, null, null, null, null);
this("1.0", null, Links.NONE, Collections.emptyList(), null, null, null);
}
@SafeVarargs
final CollectionJson<T> withItems(CollectionJsonItem<T>... items) {
return withItems(Arrays.asList(items));
}
CollectionJson<T> withItems(List<CollectionJsonItem<T>> items) {
return new CollectionJson<>(version, href, links, items, queries, template, error);
}
CollectionJson<T> withLinks(Link... links) {
return withLinks(Links.of(links));
}
CollectionJson<T> withLinks(Links links) {
return new CollectionJson<>(version, href, links, items, queries, template, error);
}
CollectionJson<T> withOwnSelfLink() {
if (href == null) {
return this;
}
return withLinks(Links.of(new Link(href)).merge(MergeMode.SKIP_BY_REL, links));
}
boolean hasItems() {
return !items.isEmpty();
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2018 the original author or authors.
* Copyright 2018-2019 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.
@@ -33,17 +33,22 @@ import org.springframework.hateoas.support.PropertyUtils;
import org.springframework.http.HttpMethod;
/**
* {@link AffordanceModel} for Collection+JSON.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
@EqualsAndHashCode(callSuper = true)
public class CollectionJsonAffordanceModel extends AffordanceModel {
class CollectionJsonAffordanceModel extends AffordanceModel {
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT,
HttpMethod.PATCH);
private final @Getter List<CollectionJsonData> inputProperties;
private final @Getter List<CollectionJsonData> queryProperties;
public CollectionJsonAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
public CollectionJsonAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
@@ -52,23 +57,20 @@ public class CollectionJsonAffordanceModel extends AffordanceModel {
}
/**
* Look at the input's domain type to extract the {@link Affordance}'s properties.
* Then transform them into a list of {@link CollectionJsonData} objects.
* Look at the input's domain type to extract the {@link Affordance}'s properties. Then transform them into a list of
* {@link CollectionJsonData} objects.
*/
private List<CollectionJsonData> determineInputs() {
if (ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
return PropertyUtils.findPropertyNames(getInputType()).stream()
.map(propertyName -> new CollectionJsonData()
.withName(propertyName)
.withValue(""))
.collect(Collectors.toList());
} else {
if (!ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
return Collections.emptyList();
}
return PropertyUtils.findPropertyNames(getInputType()).stream() //
.map(propertyName -> new CollectionJsonData() //
.withName(propertyName) //
.withValue("")) //
.collect(Collectors.toList());
}
/**
@@ -78,15 +80,14 @@ public class CollectionJsonAffordanceModel extends AffordanceModel {
*/
private List<CollectionJsonData> determineQueryProperties() {
if (getHttpMethod().equals(HttpMethod.GET)) {
return getQueryMethodParameters().stream()
.map(queryProperty -> new CollectionJsonData()
.withName(queryProperty.getName())
.withValue(""))
.collect(Collectors.toList());
} else {
if (!getHttpMethod().equals(HttpMethod.GET)) {
return Collections.emptyList();
}
return getQueryMethodParameters().stream() //
.map(queryProperty -> new CollectionJsonData() //
.withName(queryProperty.getName()) //
.withValue("")) //
.collect(Collectors.toList());
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2018 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.
@@ -22,7 +22,7 @@ import lombok.experimental.Wither;
import java.util.List;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -40,11 +40,13 @@ class CollectionJsonDocument<T> {
private CollectionJson<T> collection;
@JsonCreator
CollectionJsonDocument(@JsonProperty("version") String version, @JsonProperty("href") String href,
@JsonProperty("links") List<Link> links, @JsonProperty("items") List<CollectionJsonItem<T>> items,
@JsonProperty("queries") List<CollectionJsonQuery> queries,
@JsonProperty("template") CollectionJsonTemplate template,
@JsonProperty("error") CollectionJsonError error) {
CollectionJsonDocument(@JsonProperty("version") String version, //
@JsonProperty("href") String href, //
@JsonProperty("links") Links links, //
@JsonProperty("items") List<CollectionJsonItem<T>> items, //
@JsonProperty("queries") List<CollectionJsonQuery> queries, //
@JsonProperty("template") CollectionJsonTemplate template, //
@JsonProperty("error") CollectionJsonError error) {
this.collection = new CollectionJson<>(version, href, links, items, queries, template, error);
}
}

View File

@@ -22,11 +22,13 @@ import lombok.Value;
import lombok.experimental.Wither;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.Links.MergeMode;
import org.springframework.hateoas.support.PropertyUtils;
import com.fasterxml.jackson.annotation.JsonCreator;
@@ -48,20 +50,17 @@ class CollectionJsonItem<T> {
private String href;
private List<CollectionJsonData> data;
@JsonInclude(Include.NON_EMPTY)
private List<Link> links;
@Getter(onMethod = @__({@JsonIgnore}), value = AccessLevel.PRIVATE)
private T rawData;
private @JsonInclude(Include.NON_EMPTY) Links links;
private @Getter(onMethod = @__({ @JsonIgnore }), value = AccessLevel.PRIVATE) T rawData;
@JsonCreator
CollectionJsonItem(@JsonProperty("href") String href, @JsonProperty("data") List<CollectionJsonData> data,
@JsonProperty("links") List<Link> links) {
CollectionJsonItem(@JsonProperty("href") String href, //
@JsonProperty("data") List<CollectionJsonData> data, //
@JsonProperty("links") Links links) {
this.href = href;
this.data = data;
this.links = links;
this.links = links == null ? Links.NONE : links;
this.rawData = null;
}
@@ -72,9 +71,7 @@ class CollectionJsonItem<T> {
/**
* Simple scalar types that can be encoded by value, not type.
*/
private final static HashSet<Class<?>> PRIMITIVE_TYPES = new HashSet<Class<?>>() {{
add(String.class);
}};
private final static Set<Class<?>> PRIMITIVE_TYPES = Collections.singleton(String.class);
/**
* Transform a domain object into a collection of {@link CollectionJsonData} objects to serialize properly.
@@ -92,15 +89,13 @@ class CollectionJsonItem<T> {
}
return PropertyUtils.findProperties(this.rawData).entrySet().stream()
.map(entry -> new CollectionJsonData()
.withName(entry.getKey())
.withValue(entry.getValue()))
.collect(Collectors.toList());
.map(entry -> new CollectionJsonData().withName(entry.getKey()).withValue(entry.getValue()))
.collect(Collectors.toList());
}
/**
* Generate an object used the deserialized properties and the provided type from the deserializer.
*
*
* @param javaType - type of the object to create
* @return
*/
@@ -111,9 +106,23 @@ class CollectionJsonItem<T> {
}
return PropertyUtils.createObjectFromProperties(javaType.getRawClass(), //
this.data.stream()
.collect(Collectors.toMap(
CollectionJsonData::getName,
CollectionJsonData::getValue)));
this.data.stream().collect(Collectors.toMap(CollectionJsonData::getName, CollectionJsonData::getValue)));
}
public CollectionJsonItem<T> withLinks(Link... links) {
return new CollectionJsonItem<>(href, data, Links.of(links), rawData);
}
public CollectionJsonItem<T> withLinks(Links links) {
return new CollectionJsonItem<>(href, data, links, rawData);
}
public CollectionJsonItem<T> withOwnSelfLink() {
if (href == null) {
return this;
}
return withLinks(Links.of(new Link(href)).merge(MergeMode.SKIP_BY_REL, links));
}
}

View File

@@ -16,23 +16,23 @@
package org.springframework.hateoas.collectionjson;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Optional;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.core.JsonPathLinkDiscoverer;
import org.springframework.util.Assert;
/**
* {@link LinkDiscoverer} implementation based on JSON Collection link structure.
*
* NOTE: Since links can appear in two different places in a Collection+JSON document, this discoverer
* uses two.
* {@link LinkDiscoverer} implementation based on JSON Collection link structure. NOTE: Since links can appear in two
* different places in a Collection+JSON document, this discoverer uses two.
*
* @author Greg Turnquist
* @author Oliver Drotbohm
*/
public class CollectionJsonLinkDiscoverer extends JsonPathLinkDiscoverer {
@@ -41,77 +41,94 @@ public class CollectionJsonLinkDiscoverer extends JsonPathLinkDiscoverer {
public CollectionJsonLinkDiscoverer() {
super("$.collection..links..[?(@.rel == '%s')].href", MediaTypes.COLLECTION_JSON);
this.selfLinkDiscoverer = new CollectionJsonSelfLinkDiscoverer();
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.JsonPathLinkDiscoverer#findLinkWithRel(org.springframework.hateoas.LinkRelation, java.lang.String)
*/
@Override
public Link findLinkWithRel(String rel, String representation) {
public Optional<Link> findLinkWithRel(LinkRelation relation, String representation) {
if (rel.equals(IanaLinkRelation.SELF.value())) {
return findSelfLink(representation);
} else {
return super.findLinkWithRel(rel, representation);
}
Assert.notNull(relation, "LinkRelation must not be null!");
Assert.notNull(representation, "Representation must not be null!");
return relation.isSameAs(IanaLinkRelations.SELF) //
? findSelfLink(representation) //
: super.findLinkWithRel(relation, representation);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(java.lang.String, java.io.InputStream)
*/
@Override
public Link findLinkWithRel(String rel, InputStream representation) {
public Optional<Link> findLinkWithRel(LinkRelation relation, InputStream representation) {
if (rel.equals(IanaLinkRelation.SELF.value())) {
return findSelfLink(representation);
} else {
return super.findLinkWithRel(rel, representation);
}
Assert.notNull(relation, "LinkRelation must not be null!");
Assert.notNull(representation, "InputStream must not be null!");
return relation.isSameAs(IanaLinkRelations.SELF) //
? findSelfLink(representation) //
: super.findLinkWithRel(relation, representation);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.JsonPathLinkDiscoverer#findLinksWithRel(org.springframework.hateoas.LinkRelation, java.lang.String)
*/
@Override
public List<Link> findLinksWithRel(String rel, String representation) {
public Links findLinksWithRel(LinkRelation relation, String representation) {
if (rel.equals(IanaLinkRelation.SELF.value())) {
return addSelfLink(super.findLinksWithRel(rel, representation), representation);
} else {
return super.findLinksWithRel(rel, representation);
}
Assert.notNull(relation, "LinkRelation must not be null!");
Assert.notNull(representation, "Representation must not be null!");
return relation.isSameAs(IanaLinkRelations.SELF) //
? addSelfLink(super.findLinksWithRel(relation, representation), representation) //
: super.findLinksWithRel(relation, representation);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.JsonPathLinkDiscoverer#findLinksWithRel(org.springframework.hateoas.LinkRelation, java.io.InputStream)
*/
@Override
public List<Link> findLinksWithRel(String rel, InputStream representation) {
public Links findLinksWithRel(LinkRelation relation, InputStream representation) {
return relation.isSameAs(IanaLinkRelations.SELF) //
? addSelfLink(super.findLinksWithRel(relation, representation), representation) //
: super.findLinksWithRel(relation, representation);
if (rel.equals(IanaLinkRelation.SELF.value())) {
return addSelfLink(super.findLinksWithRel(rel, representation), representation);
} else {
return super.findLinksWithRel(rel, representation);
}
}
//
// Internal methods to support discovering the "self" link found at "$.collection.href".
//
private Link findSelfLink(String representation) {
return this.selfLinkDiscoverer.findLinkWithRel(IanaLinkRelation.SELF.value(), representation);
private Optional<Link> findSelfLink(String representation) {
return this.selfLinkDiscoverer.findLinkWithRel(IanaLinkRelations.SELF, representation);
}
private Link findSelfLink(InputStream representation) {
return this.selfLinkDiscoverer.findLinkWithRel(IanaLinkRelation.SELF.value(), representation);
private Optional<Link> findSelfLink(InputStream representation) {
return this.selfLinkDiscoverer.findLinkWithRel(IanaLinkRelations.SELF, representation);
}
private List<Link> addSelfLink(List<Link> links, String representation) {
private Links addSelfLink(Links links, String representation) {
return Stream.concat(
Stream.of(findSelfLink(representation)),
links.stream()
)
.collect(Collectors.toList());
return findSelfLink(representation) //
.map(Links::of) //
.map(it -> it.and(links)) //
.orElseGet(() -> links);
}
private List<Link> addSelfLink(List<Link> links, InputStream representation) {
private Links addSelfLink(Links links, InputStream representation) {
return Stream.concat(
Stream.of(findSelfLink(representation)),
links.stream()
)
.collect(Collectors.toList());
return findSelfLink(representation) //
.map(Links::of) //
.map(it -> it.and(links)) //
.orElseGet(() -> links);
}
/**

View File

@@ -15,12 +15,11 @@
*/
package org.springframework.hateoas.collectionjson;
import static org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.*;
import java.util.List;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonLinksDeserializer;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonLinksSerializer;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonResourceSupportDeserializer;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -39,8 +38,8 @@ abstract class ResourceSupportMixin extends ResourceSupport {
@Override
@JsonProperty("collection")
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonSerialize(using = CollectionJsonLinkListSerializer.class)
@JsonDeserialize(using = CollectionJsonLinkListDeserializer.class)
public abstract List<Link> getLinks();
@JsonSerialize(using = CollectionJsonLinksSerializer.class)
@JsonDeserialize(using = CollectionJsonLinksDeserializer.class)
public abstract Links getLinks();
}

View File

@@ -33,7 +33,6 @@ import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule;
import org.springframework.hateoas.collectionjson.Jackson2CollectionJsonModule.CollectionJsonHandlerInstantiator;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.core.DelegatingRelProvider;
import org.springframework.hateoas.hal.CurieProvider;
@@ -45,7 +44,6 @@ import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsHandlerInstantiator;
import org.springframework.hateoas.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import org.springframework.hateoas.uber.Jackson2UberModule;
import org.springframework.hateoas.uber.Jackson2UberModule.UberHandlerInstantiator;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
@@ -60,7 +58,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
*/
@Configuration
@RequiredArgsConstructor
public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware {
class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, BeanFactoryAware {
private static final String MESSAGE_SOURCE_BEAN_NAME = "linkRelationMessageSource";
@@ -73,7 +71,7 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
private BeanFactory beanFactory;
private Collection<HypermediaType> hypermediaTypes;
/*
/*
* (non-Javadoc)
* @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
*/
@@ -89,24 +87,23 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
this.hypermediaTypes = hyperMediaTypes;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.web.servlet.config.annotation.WebMvcConfigurer#extendMessageConverters(java.util.List)
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
if (converters.stream()
.filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
if (converters.stream().filter(MappingJackson2HttpMessageConverter.class::isInstance)
.map(AbstractJackson2HttpMessageConverter.class::cast)
.map(AbstractJackson2HttpMessageConverter::getObjectMapper)
.anyMatch(Jackson2HalModule::isAlreadyRegisteredIn)) {
return;
}
ObjectMapper objectMapper = mapper.getIfAvailable(ObjectMapper::new);
MessageSourceAccessor linkRelationMessageSource = beanFactory.getBean(MESSAGE_SOURCE_BEAN_NAME,
MessageSourceAccessor.class);
@@ -123,7 +120,7 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
converters.add(0, createHalFormsConverter(objectMapper, curieProvider, relProvider, linkRelationMessageSource));
}
}
if (hypermediaTypes.contains(HypermediaType.COLLECTION_JSON)) {
converters.add(0, createCollectionJsonConverter(objectMapper, linkRelationMessageSource));
}
@@ -143,13 +140,12 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2UberModule());
mapper.setHandlerInstantiator(new UberHandlerInstantiator());
// mapper.setHandlerInstantiator(new UberHandlerInstantiator());
return new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Collections.singletonList(UBER_JSON), mapper);
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(UBER_JSON), mapper);
}
/**
* @param objectMapper
* @param linkRelationMessageSource
@@ -162,10 +158,9 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2CollectionJsonModule());
mapper.setHandlerInstantiator(new CollectionJsonHandlerInstantiator(linkRelationMessageSource));
return new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Collections.singletonList(COLLECTION_JSON), mapper);
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(COLLECTION_JSON), mapper);
}
/**
@@ -182,12 +177,11 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalFormsModule());
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(
relProvider, curieProvider, linkRelationMessageSource, true,
this.halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new)));
mapper.setHandlerInstantiator(new HalFormsHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
true, this.halFormsConfiguration.getIfAvailable(HalFormsConfiguration::new)));
return new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Collections.singletonList(HAL_FORMS_JSON), mapper);
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Collections.singletonList(HAL_FORMS_JSON), mapper);
}
/**
@@ -201,13 +195,13 @@ public class ConverterRegisteringWebMvcConfigurer implements WebMvcConfigurer, B
RelProvider relProvider, MessageSourceAccessor linkRelationMessageSource) {
ObjectMapper mapper = objectMapper.copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jackson2HalModule());
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider,
linkRelationMessageSource, this.halConfiguration.getIfAvailable(HalConfiguration::new)));
mapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, linkRelationMessageSource,
this.halConfiguration.getIfAvailable(HalConfiguration::new)));
return new TypeConstrainedMappingJackson2HttpMessageConverter(
ResourceSupport.class, Arrays.asList(HAL_JSON, HAL_JSON_UTF8), mapper);
return new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class,
Arrays.asList(HAL_JSON, HAL_JSON_UTF8), mapper);
}
}

View File

@@ -20,6 +20,7 @@ import java.util.Map;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.RelProvider;
/**
@@ -41,7 +42,7 @@ public class AnnotationRelProvider implements RelProvider, Ordered {
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
*/
@Override
public String getCollectionResourceRelFor(Class<?> type) {
public LinkRelation getCollectionResourceRelFor(Class<?> type) {
Relation annotation = lookupAnnotation(type);
@@ -49,15 +50,15 @@ public class AnnotationRelProvider implements RelProvider, Ordered {
return null;
}
return annotation.collectionRelation();
return LinkRelation.of(annotation.collectionRelation());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Object)
*/
@Override
public String getItemResourceRelFor(Class<?> type) {
public LinkRelation getItemResourceRelFor(Class<?> type) {
Relation annotation = lookupAnnotation(type);
@@ -65,10 +66,10 @@ public class AnnotationRelProvider implements RelProvider, Ordered {
return null;
}
return annotation.value();
return LinkRelation.of(annotation.value());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/

View File

@@ -16,13 +16,14 @@
package org.springframework.hateoas.core;
import org.springframework.core.Ordered;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.RelProvider;
import org.springframework.util.StringUtils;
/**
* Default implementation of {@link RelProvider} to simply use the uncapitalized version of the given type's name as
* single resource rel as well as an appended {@code List} for the collection resource rel.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -42,21 +43,21 @@ public class DefaultRelProvider implements RelProvider, Ordered {
return true;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
*/
@Override
public String getCollectionResourceRelFor(Class<?> type) {
return StringUtils.uncapitalize(type.getSimpleName()) + "List";
public LinkRelation getCollectionResourceRelFor(Class<?> type) {
return LinkRelation.of(StringUtils.uncapitalize(type.getSimpleName()) + "List");
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
*/
@Override
public String getItemResourceRelFor(Class<?> type) {
return StringUtils.uncapitalize(type.getSimpleName());
public LinkRelation getItemResourceRelFor(Class<?> type) {
return LinkRelation.of(StringUtils.uncapitalize(type.getSimpleName()));
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.hateoas.core;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.RelProvider;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
@@ -32,12 +33,12 @@ public class DelegatingRelProvider implements RelProvider {
this.providers = providers;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
*/
@Override
public String getItemResourceRelFor(Class<?> type) {
public LinkRelation getItemResourceRelFor(Class<?> type) {
return providers.getRequiredPluginFor(type).getItemResourceRelFor(type);
}
@@ -46,7 +47,7 @@ public class DelegatingRelProvider implements RelProvider {
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
*/
@Override
public String getCollectionResourceRelFor(java.lang.Class<?> type) {
public LinkRelation getCollectionResourceRelFor(java.lang.Class<?> type) {
return providers.getRequiredPluginFor(type).getCollectionResourceRelFor(type);
}

View File

@@ -15,11 +15,14 @@
*/
package org.springframework.hateoas.core;
import java.util.Optional;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Resource;
/**
* A wrapper to handle values to be embedded into a {@link Resource}.
*
*
* @author Oliver Gierke
*/
public interface EmbeddedWrapper {
@@ -28,30 +31,30 @@ public interface EmbeddedWrapper {
* Returns the rel to be used when embedding. If this returns {@literal null}, the rel will be calculated based on the
* type returned by {@link #getRelTargetType()}. A wrapper returning {@literal null} for both {@link #getRel()} and
* {@link #getRelTargetType()} is considered invalid.
*
*
* @return
* @see #getRelTargetType()
*/
String getRel();
Optional<LinkRelation> getRel();
/**
* Returns whether the wrapper has the given rel.
*
*
* @param rel can be {@literal null}.
* @return
*/
boolean hasRel(String rel);
boolean hasRel(LinkRelation rel);
/**
* Returns whether the wrapper is a collection value.
*
*
* @return
*/
boolean isCollectionValue();
/**
* Returns the actual value to embed.
*
*
* @return
*/
Object getValue();
@@ -60,7 +63,7 @@ public interface EmbeddedWrapper {
* Returns the type to be used to calculate a type based rel. Can return {@literal null} in case an explicit rel is
* returned in {@link #getRel()}. A wrapper returning {@literal null} for both {@link #getRel()} and
* {@link #getRelTargetType()} is considered invalid.
*
*
* @return
*/
Class<?> getRelTargetType();

View File

@@ -17,14 +17,16 @@ package org.springframework.hateoas.core;
import java.util.Collection;
import java.util.Collections;
import java.util.Optional;
import org.springframework.aop.support.AopUtils;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Resource;
import org.springframework.util.Assert;
/**
* Interface to mark objects that are aware of the rel they'd like to be exposed under.
*
*
* @author Oliver Gierke
*/
public class EmbeddedWrappers {
@@ -33,7 +35,7 @@ public class EmbeddedWrappers {
/**
* Creates a new {@link EmbeddedWrappers}.
*
*
* @param preferCollections whether wrappers for single elements should rather treat the value as collection.
*/
public EmbeddedWrappers(boolean preferCollections) {
@@ -42,7 +44,7 @@ public class EmbeddedWrappers {
/**
* Creates a new {@link EmbeddedWrapper} that
*
*
* @param source
* @return
*/
@@ -52,7 +54,7 @@ public class EmbeddedWrappers {
/**
* Creates an {@link EmbeddedWrapper} for an empty {@link Collection} with the given element type.
*
*
* @param type must not be {@literal null}.
* @return
*/
@@ -62,13 +64,13 @@ public class EmbeddedWrappers {
/**
* Creates a new {@link EmbeddedWrapper} with the given rel.
*
*
* @param source can be {@literal null}, will return {@literal null} if so.
* @param rel must not be {@literal null} or empty.
* @return
*/
@SuppressWarnings("unchecked")
public EmbeddedWrapper wrap(Object source, String rel) {
public EmbeddedWrapper wrap(Object source, LinkRelation rel) {
if (source == null) {
return null;
@@ -91,40 +93,42 @@ public class EmbeddedWrappers {
private static abstract class AbstractEmbeddedWrapper implements EmbeddedWrapper {
private static final String NO_REL = "___norel___";
private static final LinkRelation NO_REL = LinkRelation.of("___norel___");
private final String rel;
private final LinkRelation rel;
/**
* Creates a new {@link AbstractEmbeddedWrapper} with the given rel.
*
*
* @param rel must not be {@literal null} or empty.
*/
public AbstractEmbeddedWrapper(String rel) {
public AbstractEmbeddedWrapper(LinkRelation rel) {
Assert.hasText(rel, "Rel must not be null or empty!");
Assert.notNull(rel, "Rel must not be null or empty!");
this.rel = rel;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getRel()
*/
@Override
public String getRel() {
return NO_REL.equals(rel) ? null : rel;
public Optional<LinkRelation> getRel() {
return Optional.ofNullable(rel) //
.filter(it -> !it.equals(NO_REL));
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#hasRel(java.lang.String)
* @see org.springframework.hateoas.core.EmbeddedWrapper#hasRel(org.springframework.hateoas.LinkRelation)
*/
@Override
public boolean hasRel(String rel) {
return this.rel.equals(rel);
public boolean hasRel(LinkRelation rel) {
return this.rel.isSameAs(rel);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getRelTargetType()
*/
@@ -145,7 +149,7 @@ public class EmbeddedWrappers {
/**
* Peek into the wrapped element. The object returned is used to determine the actual value type of the wrapper.
*
*
* @return
*/
protected abstract Object peek();
@@ -161,19 +165,19 @@ public class EmbeddedWrappers {
private final Object value;
/**
* Creates a new {@link EmbeddedElement} for the given value and rel.
*
* Creates a new {@link EmbeddedElement} for the given value and link relation.
*
* @param value must not be {@literal null}.
* @param rel must not be {@literal null} or empty.
* @param relation must not be {@literal null}.
*/
public EmbeddedElement(Object value, String rel) {
public EmbeddedElement(Object value, LinkRelation relation) {
super(rel);
super(relation);
Assert.notNull(value, "Value must not be null!");
this.value = value;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getValue()
*/
@@ -182,7 +186,7 @@ public class EmbeddedWrappers {
return value;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.EmbeddedWrappers.AbstractElementWrapper#peek()
*/
@@ -191,7 +195,7 @@ public class EmbeddedWrappers {
return getValue();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#isCollectionValue()
*/
@@ -214,7 +218,7 @@ public class EmbeddedWrappers {
* @param value must not be {@literal null} or empty.
* @param rel must not be {@literal null} or empty.
*/
public EmbeddedCollection(Collection<Object> value, String rel) {
public EmbeddedCollection(Collection<Object> value, LinkRelation rel) {
super(rel);
@@ -227,7 +231,7 @@ public class EmbeddedWrappers {
this.value = value;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.EmbeddedWrapper#getValue()
*/
@@ -267,7 +271,7 @@ public class EmbeddedWrappers {
/**
* Creates a new {@link EmptyCollectionEmbeddedWrapper}.
*
*
* @param type must not be {@literal null}.
*/
public EmptyCollectionEmbeddedWrapper(Class<?> type) {
@@ -276,16 +280,16 @@ public class EmbeddedWrappers {
this.type = type;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#getRel()
*/
@Override
public String getRel() {
return null;
public Optional<LinkRelation> getRel() {
return Optional.empty();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#getValue()
*/
@@ -294,7 +298,7 @@ public class EmbeddedWrappers {
return Collections.emptySet();
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#getRelTargetType()
*/
@@ -303,7 +307,7 @@ public class EmbeddedWrappers {
return type;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#isCollectionValue()
*/
@@ -312,12 +316,12 @@ public class EmbeddedWrappers {
return true;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.EmbeddedWrapper#hasRel(java.lang.String)
* @see org.springframework.hateoas.core.EmbeddedWrapper#hasRel(org.springframework.hateoas.LinkRelation)
*/
@Override
public boolean hasRel(String rel) {
public boolean hasRel(LinkRelation rel) {
return false;
}
}

View File

@@ -16,12 +16,13 @@
package org.springframework.hateoas.core;
import org.atteo.evo.inflector.English;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.RelProvider;
/**
* {@link RelProvider} implementation using the Evo Inflector implementation of an algorithmic approach to English
* plurals.
*
*
* @see http://www.csse.monash.edu.au/~damian/papers/HTML/Plurals.html
* @author Oliver Gierke
*/
@@ -32,7 +33,7 @@ public class EvoInflectorRelProvider extends DefaultRelProvider {
* @see org.springframework.hateoas.core.DefaultRelProvider#getCollectionResourceRelFor(java.lang.Class)
*/
@Override
public String getCollectionResourceRelFor(Class<?> type) {
return English.plural(getItemResourceRelFor(type));
public LinkRelation getCollectionResourceRelFor(Class<?> type) {
return LinkRelation.of(English.plural(getItemResourceRelFor(type).value()));
}
}

View File

@@ -20,14 +20,17 @@ import net.minidev.json.JSONArray;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.http.MediaType;
import org.springframework.util.Assert;
@@ -63,51 +66,51 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer {
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(java.lang.String, java.lang.String)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(org.springframework.hateoas.LinkRelation, java.lang.String)
*/
@Override
public Link findLinkWithRel(String rel, String representation) {
List<Link> links = findLinksWithRel(rel, representation);
return links.isEmpty() ? null : links.get(0);
public Optional<Link> findLinkWithRel(LinkRelation relation, String representation) {
return firstOrEmpty(findLinksWithRel(relation, representation));
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(java.lang.String, java.io.InputStream)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(org.springframework.hateoas.LinkRelation, java.io.InputStream)
*/
@Override
public Link findLinkWithRel(String rel, InputStream representation) {
List<Link> links = findLinksWithRel(rel, representation);
return links.isEmpty() ? null : links.get(0);
public Optional<Link> findLinkWithRel(LinkRelation relation, InputStream representation) {
return firstOrEmpty(findLinksWithRel(relation, representation));
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(java.lang.String, java.lang.String)
* @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(org.springframework.hateoas.LinkRelation, java.lang.String)
*/
@Override
public List<Link> findLinksWithRel(String rel, String representation) {
public Links findLinksWithRel(LinkRelation relation, String representation) {
Assert.notNull(relation, "LinkRelation must not be null!");
try {
Object parseResult = getExpression(rel).read(representation);
return createLinksFrom(parseResult, rel);
Object parseResult = getExpression(relation).read(representation);
return createLinksFrom(parseResult, relation);
} catch (InvalidPathException e) {
return Collections.emptyList();
return Links.NONE;
}
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(java.lang.String, java.io.InputStream)
* @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(org.springframework.hateoas.LinkRelation, java.io.InputStream)
*/
@Override
public List<Link> findLinksWithRel(String rel, InputStream representation) {
public Links findLinksWithRel(LinkRelation relation, InputStream representation) {
Assert.notNull(relation, "LinkRelation must not be null!");
try {
Object parseResult = getExpression(rel).read(representation);
return createLinksFrom(parseResult, rel);
Object parseResult = getExpression(relation).read(representation);
return createLinksFrom(parseResult, relation);
} catch (IOException e) {
throw new RuntimeException(e);
}
@@ -131,7 +134,7 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer {
* @param rel
* @return link
*/
protected Link extractLink(Object element, String rel) {
protected Link extractLink(Object element, LinkRelation rel) {
return new Link(element.toString(), rel);
}
@@ -141,8 +144,8 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer {
* @param rel
* @return
*/
private JsonPath getExpression(String rel) {
return JsonPath.compile(String.format(pathTemplate, rel));
private JsonPath getExpression(LinkRelation rel) {
return JsonPath.compile(String.format(pathTemplate, rel.value()));
}
/**
@@ -152,7 +155,7 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer {
* @param rel the relation type that was parsed for.
* @return
*/
private List<Link> createLinksFrom(Object parseResult, String rel) {
private Links createLinksFrom(Object parseResult, LinkRelation rel) {
if (parseResult instanceof JSONArray) {
@@ -161,11 +164,18 @@ public class JsonPathLinkDiscoverer implements LinkDiscoverer {
return jsonArray.stream() //
.flatMap(it -> JSONArray.class.isInstance(it) ? ((JSONArray) it).stream() : Stream.of(it)) //
.map(it -> extractLink(it, rel)) //
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
.collect(Collectors.collectingAndThen(Collectors.toList(), Links::of));
}
return parseResult instanceof Map //
? Collections.singletonList(extractLink(parseResult, rel)) //
: Collections.singletonList(new Link(parseResult.toString(), rel));
return Links.of(parseResult instanceof Map //
? extractLink(parseResult, rel) //
: new Link(parseResult.toString(), rel));
}
private static <T> Optional<T> firstOrEmpty(Iterable<T> source) {
Iterator<T> iterator = source.iterator();
return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty();
}
}

View File

@@ -27,10 +27,11 @@ import java.util.List;
import java.util.Optional;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Identifiable;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkBuilder;
import org.springframework.hateoas.LinkRelation;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents;
@@ -38,7 +39,7 @@ import org.springframework.web.util.UriComponentsBuilder;
/**
* Base class to implement {@link LinkBuilder}s based on a Spring MVC {@link UriComponentsBuilder}.
*
*
* @author Ricardo Gladwell
* @author Oliver Gierke
* @author Kamill Sokol
@@ -53,7 +54,7 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
/**
* Creates a new {@link LinkBuilderSupport} using the given {@link UriComponentsBuilder}.
*
*
* @param builder must not be {@literal null}.
*/
public LinkBuilderSupport(UriComponentsBuilder builder) {
@@ -153,10 +154,12 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkBuilder#withRel(java.lang.String)
* @see org.springframework.hateoas.LinkBuilder#withRel(org.springframework.hateoas.LinkRelation)
*/
public Link withRel(String rel) {
return new Link(toString(), rel).withAffordances(affordances);
public Link withRel(LinkRelation rel) {
return new Link(toString(), rel) //
.withAffordances(affordances);
}
/*
@@ -164,7 +167,7 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
* @see org.springframework.hateoas.LinkBuilder#withSelfRel()
*/
public Link withSelfRel() {
return withRel(IanaLinkRelation.SELF.value());
return withRel(IanaLinkRelations.SELF);
}
/*
@@ -178,14 +181,14 @@ public abstract class LinkBuilderSupport<T extends LinkBuilder> implements LinkB
/**
* Returns the current concrete instance.
*
*
* @return
*/
protected abstract T getThis();
/**
* Creates a new instance of the sub-class.
*
*
* @param builder will never be {@literal null}.
* @return
*/

View File

@@ -1,35 +0,0 @@
/*
* Copyright 2018 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
*
* 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 org.springframework.hateoas.core;
/**
* Utilities to mimic {@link java.util.Objects} helper methods.
*
* @author Greg Turnquist
*/
public final class Objects {
/**
* Variant of {@link java.util.Objects#requireNonNull(Object, String)} that throws an {@link IllegalArgumentException}.
*/
public static <T> T requireNonNull(T obj, String message) {
if (obj == null) {
throw new IllegalArgumentException(message);
}
return obj;
}
}

View File

@@ -18,11 +18,12 @@ package org.springframework.hateoas.hal;
import java.util.Collection;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
/**
* API to provide HAL curie information for links.
*
*
* @see http://tools.ietf.org/html/draft-kelly-json-hal#section-8.2
* @author Oliver Gierke
* @author Jeff Stano
@@ -33,26 +34,26 @@ public interface CurieProvider {
/**
* Returns the rel to be rendered for the given {@link Link}. Will potentially prefix the rel but also might decide
* not to, depending on the actual rel.
*
*
* @param link
* @return
*/
String getNamespacedRelFrom(Link link);
HalLinkRelation getNamespacedRelFrom(Link link);
/**
* Returns the rel to be rendered for the given rel. Will potentially prefix the rel but also might decide not to,
* depending on the actual rel.
*
*
* @param rel
* @return
* @since 0.17
*/
String getNamespacedRelFor(String rel);
HalLinkRelation getNamespacedRelFor(LinkRelation rel);
/**
* Returns an object to render as the base curie information. Implementations have to make sure, the returned
* instances renders as defined in the spec.
*
*
* @param links the {@link Links} that have been added to the response so far.
* @return must not be {@literal null}.
*/

View File

@@ -22,8 +22,8 @@ import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.UriTemplate;
import org.springframework.util.Assert;
@@ -32,7 +32,7 @@ import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/**
* Default implementation of {@link CurieProvider} rendering a single configurable {@link UriTemplate} based curie.
*
*
* @author Oliver Gierke
* @author Jeff Stano
* @author Greg Turnquist
@@ -46,7 +46,7 @@ public class DefaultCurieProvider implements CurieProvider {
/**
* Creates a new {@link DefaultCurieProvider} for the given name and {@link UriTemplate}. The curie will be used to
* expand previously unprefixed, non-IANA link relations.
*
*
* @param name must not be {@literal null} or empty.
* @param uriTemplate must not be {@literal null} and contain exactly one template variable.
*/
@@ -58,7 +58,7 @@ public class DefaultCurieProvider implements CurieProvider {
* Creates a new {@link DefaultCurieProvider} for the given curies. If more than one curie is given, no default curie
* will be registered. Use {@link #DefaultCurieProvider(Map, String)} to define which of the provided curies shall be
* used as the default one.
*
*
* @param curies must not be {@literal null}.
* @see #DefaultCurieProvider(String, UriTemplate)
* @since 0.19
@@ -70,7 +70,7 @@ public class DefaultCurieProvider implements CurieProvider {
/**
* Creates a new {@link DefaultCurieProvider} for the given curies using the one with the given name as default, which
* means to expand unprefixed, non-IANA link relations.
*
*
* @param curies must not be {@literal null}.
* @param defaultCurieName can be {@literal null}.
* @since 0.19
@@ -92,7 +92,7 @@ public class DefaultCurieProvider implements CurieProvider {
this.curies = Collections.unmodifiableMap(curies);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.CurieProvider#getCurieInformation()
*/
@@ -104,30 +104,31 @@ public class DefaultCurieProvider implements CurieProvider {
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableCollection));
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(org.springframework.hateoas.Link)
*/
@Override
public String getNamespacedRelFrom(Link link) {
public HalLinkRelation getNamespacedRelFrom(Link link) {
return getNamespacedRelFor(link.getRel());
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(java.lang.String)
*/
@Override
public String getNamespacedRelFor(String rel) {
public HalLinkRelation getNamespacedRelFor(LinkRelation relation) {
boolean prefixingNeeded = defaultCurie != null && !IanaLinkRelation.isIanaRel(rel) && !rel.contains(":");
return prefixingNeeded ? String.format("%s:%s", defaultCurie, rel) : rel;
HalLinkRelation result = HalLinkRelation.of(relation);
return defaultCurie == null ? result : result.curieIfUncuried(defaultCurie);
}
/**
* Returns the href for the {@link Curie} instance to be created. Will prepend the current application URI (servlet
* mapping) in case the template is not an absolute one in the first place.
*
*
* @param name will never be {@literal null} or empty.
* @param template will never be {@literal null}.
* @return the {@link String} to be used as href in the {@link Curie} to be created, must not be {@literal null}.
@@ -144,7 +145,7 @@ public class DefaultCurieProvider implements CurieProvider {
/**
* Value object to get the curie {@link Link} rendered in JSON.
*
*
* @author Oliver Gierke
*/
protected static class Curie extends Link {

View File

@@ -22,33 +22,33 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.core.EmbeddedWrapper;
import org.springframework.hateoas.core.EmbeddedWrappers;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
* Builder class that allows collecting objects under the relation types defined for the objects but moving from the
* single resource relation to the collection one, once more than one object of the same type is added.
*
*
* @author Oliver Gierke
* @author Dietrich Schulten
*/
class HalEmbeddedBuilder {
private static final String DEFAULT_REL = "content";
private static final HalLinkRelation DEFAULT_REL = HalLinkRelation.uncuried("content");
private static final String INVALID_EMBEDDED_WRAPPER = "Embedded wrapper %s returned null for both the static rel and the rel target type! Make sure one of the two returns a non-null value!";
private final Map<String, Object> embeddeds = new HashMap<>();
private final Map<HalLinkRelation, Object> embeddeds = new HashMap<>();
private final RelProvider provider;
private final CurieProvider curieProvider;
private final EmbeddedWrappers wrappers;
/**
* Creates a new {@link HalEmbeddedBuilder} using the given {@link RelProvider} and prefer collection rels flag.
*
*
* @param provider can be {@literal null}.
* @param preferCollectionRels whether to prefer to ask the provider for collection rels.
*/
@@ -64,7 +64,7 @@ class HalEmbeddedBuilder {
/**
* Adds the given value to the embeddeds. Will skip doing so if the value is {@literal null} or the content of a
* {@link Resource} is {@literal null}.
*
*
* @param source can be {@literal null}.
*/
public void add(Object source) {
@@ -75,8 +75,8 @@ class HalEmbeddedBuilder {
return;
}
String collectionRel = getDefaultedRelFor(wrapper, true);
String collectionOrItemRel = collectionRel;
HalLinkRelation collectionRel = getDefaultedRelFor(wrapper, true);
HalLinkRelation collectionOrItemRel = collectionRel;
if (!embeddeds.containsKey(collectionRel)) {
collectionOrItemRel = getDefaultedRelFor(wrapper, wrapper.isCollectionValue());
@@ -100,43 +100,47 @@ class HalEmbeddedBuilder {
@SuppressWarnings("unchecked")
private Collection<Object> asCollection(Object source) {
return source instanceof Collection ? (Collection<Object>) source : source == null ? Collections.emptySet()
: Collections.singleton(source);
return source instanceof Collection //
? (Collection<Object>) source //
: source == null ? Collections.emptySet() : Collections.singleton(source);
}
private String getDefaultedRelFor(EmbeddedWrapper wrapper, boolean forCollection) {
private HalLinkRelation getDefaultedRelFor(EmbeddedWrapper wrapper, boolean forCollection) {
String valueRel = wrapper.getRel();
return wrapper.getRel() //
.map(HalLinkRelation::of) //
.orElseGet(() -> {
if (StringUtils.hasText(valueRel)) {
return valueRel;
}
if (provider == null) {
return DEFAULT_REL;
}
if (provider == null) {
return DEFAULT_REL;
}
Class<?> type = wrapper.getRelTargetType();
Class<?> type = wrapper.getRelTargetType();
if (type == null) {
throw new IllegalStateException(String.format(INVALID_EMBEDDED_WRAPPER, wrapper));
}
if (type == null) {
throw new IllegalStateException(String.format(INVALID_EMBEDDED_WRAPPER, wrapper));
}
LinkRelation rel = forCollection //
? provider.getCollectionResourceRelFor(type) //
: provider.getItemResourceRelFor(type);
String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type);
if (curieProvider != null) {
rel = curieProvider.getNamespacedRelFor(rel);
}
if (curieProvider != null) {
rel = curieProvider.getNamespacedRelFor(rel);
}
return rel == null ? DEFAULT_REL : HalLinkRelation.of(rel);
return rel == null ? DEFAULT_REL : rel;
});
}
/**
* Returns the added objects keyed up by their relation types.
*
*
* @return
*/
public Map<String, Object> asMap() {
public Map<HalLinkRelation, Object> asMap() {
return Collections.unmodifiableMap(embeddeds);
}
}

View File

@@ -19,12 +19,14 @@ import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.core.JsonPathLinkDiscoverer;
import org.springframework.http.MediaType;
/**
* {@link LinkDiscoverer} implementation based on HAL link structure.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -34,27 +36,34 @@ public class HalLinkDiscoverer extends JsonPathLinkDiscoverer {
* Constructor for {@link MediaTypes#HAL_JSON}.
*/
public HalLinkDiscoverer() {
super("$._links..['%s']", MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
this(MediaTypes.HAL_JSON, MediaTypes.HAL_JSON_UTF8);
}
protected HalLinkDiscoverer(MediaType... mediaTypes) {
super("$._links..['%s']", mediaTypes);
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.core.JsonPathLinkDiscoverer#extractLink(java.lang.Object, org.springframework.hateoas.LinkRelation)
*/
@Override
@SuppressWarnings("unchecked")
protected Link extractLink(Object element, String rel) {
protected Link extractLink(Object element, LinkRelation rel) {
if (element instanceof Map) {
Map<String, String> json = (Map<String, String>) element;
return new Link(json.get("href"), rel)
.withHreflang(json.get("hreflang"))
.withMedia(json.get("media"))
.withTitle(json.get("title"))
.withType(json.get("type"))
.withDeprecation(json.get("deprecation"))
.withProfile(json.get("profile"))
.withName(json.get("name"));
if (!Map.class.isInstance(element)) {
return super.extractLink(element, rel);
}
return super.extractLink(element, rel);
Map<String, String> json = (Map<String, String>) element;
return new Link(json.get("href"), rel) //
.withHreflang(json.get("hreflang")) //
.withMedia(json.get("media")) //
.withTitle(json.get("title")) //
.withType(json.get("type")) //
.withDeprecation(json.get("deprecation")) //
.withProfile(json.get("profile")) //
.withName(json.get("name"));
}
}

View File

@@ -0,0 +1,173 @@
/*
* Copyright 2019 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
*
* 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 org.springframework.hateoas.hal;
import lombok.AccessLevel;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import java.util.stream.Stream;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.LinkRelation;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
/**
* Value object for HAL based {@link LinkRelation}, i.e. a relation that can be curied.
*
* @author Oliver Drotbohm
*/
@EqualsAndHashCode
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class HalLinkRelation implements LinkRelation, MessageSourceResolvable {
public static final HalLinkRelation CURIES = HalLinkRelation.uncuried("curies");
private static final String RELATION_MESSAGE_TEMPLATE = "_links.%s.title";
private final String curie;
private final @NonNull @Getter String localPart;
/**
* Returns a {@link HalLinkRelation} for the given general {@link LinkRelation}.
*
* @param relation must not be {@literal null}.
* @return
*/
public static HalLinkRelation of(LinkRelation relation) {
Assert.notNull(relation, "LinkRelation must not be null!");
if (HalLinkRelation.class.isInstance(relation)) {
return HalLinkRelation.class.cast(relation);
}
return of(relation.value());
}
/**
* Creates a new {@link HalLinkRelation} from the given link relation {@link String}.
*
* @param relation must not be {@literal null}.
* @return
*/
@JsonCreator
private static HalLinkRelation of(String relation) {
String[] split = relation.split(":");
String curie = split.length == 1 ? null : split[0];
String localPart = split.length == 1 ? split[0] : split[1];
return new HalLinkRelation(curie, localPart);
}
/**
* Creates a new {@link HalLinkRelation} for a curied relation.
*
* @param curie the curie, must not be {@literal null} or empty.
* @param rel the link relation to be used, must not be {@literal null}.
* @return
*/
public static HalLinkRelation curied(String curie, String rel) {
Assert.hasText(curie, "Curie must not be null or empty!");
return new HalLinkRelation(curie, rel);
}
/**
* Creates a new uncuried {@link HalLinkRelation}.
*
* @param rel the link relation to be used, must not be {@literal null}.
* @return
*/
public static HalLinkRelation uncuried(String rel) {
return new HalLinkRelation(null, rel);
}
/**
* Creates a new {@link HalLinkRelation} curied to the given value.
*
* @param curie must not be {@literal null} or empty.
* @return
*/
public HalLinkRelation curie(String curie) {
Assert.hasText(curie, "Curie must not be null or empty!");
return new HalLinkRelation(curie, localPart);
}
/**
* Returns a curied {@link HalLinkRelation} either using the existing curie or the given one if previously uncuried.
*
* @param curie must not be {@literal null} or empty.
* @return
*/
public HalLinkRelation curieIfUncuried(String curie) {
Assert.hasText(curie, "Curie must not be null or empty!");
return isCuried() || IanaLinkRelations.isIanaRel(localPart) ? this : curie(curie);
}
/**
* Returns whether the link relation is curied.
*
* @return
*/
public boolean isCuried() {
return curie != null;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkRelation#value()
*/
@JsonValue
@Override
public String value() {
return isCuried() ? String.format("%s:%s", curie, localPart) : localPart;
}
/*
* (non-Javadoc)
* @see org.springframework.context.MessageSourceResolvable#getCodes()
*/
@Override
public String[] getCodes() {
return Stream.of(value(), localPart) //
.map(it -> String.format(RELATION_MESSAGE_TEMPLATE, it)) //
.toArray(String[]::new);
}
/*
* (non-Javadoc)
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return value();
}
}

View File

@@ -31,6 +31,7 @@ import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.NoSuchMessageException;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
@@ -103,12 +104,10 @@ public class Jackson2HalModule extends SimpleModule {
* @author Alexander Baetz
* @author Oliver Gierke
*/
public static class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
public static class HalLinkListSerializer extends ContainerSerializer<Links> implements ContextualSerializer {
private static final long serialVersionUID = -1844788111509966406L;
private static final String RELATION_MESSAGE_TEMPLATE = "_links.%s.title";
private final BeanProperty property;
private final CurieProvider curieProvider;
private final EmbeddedMapper mapper;
@@ -116,14 +115,14 @@ public class Jackson2HalModule extends SimpleModule {
private final HalConfiguration halConfiguration;
public HalLinkListSerializer(CurieProvider curieProvider, EmbeddedMapper mapper, MessageSourceAccessor accessor,
HalConfiguration halConfiguration) {
HalConfiguration halConfiguration) {
this(null, curieProvider, mapper, accessor, halConfiguration);
}
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, EmbeddedMapper mapper,
MessageSourceAccessor accessor, HalConfiguration halConfiguration) {
MessageSourceAccessor accessor, HalConfiguration halConfiguration) {
super(TypeFactory.defaultInstance().constructType(List.class));
super(TypeFactory.defaultInstance().constructType(Links.class));
this.property = property;
this.curieProvider = curieProvider;
@@ -144,11 +143,10 @@ public class Jackson2HalModule extends SimpleModule {
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
public void serialize(Links value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<>();
Map<LinkRelation, List<Object>> sortedLinks = new LinkedHashMap<>();
List<Link> links = new ArrayList<>();
boolean prefixingRequired = curieProvider != null;
@@ -169,15 +167,15 @@ public class Jackson2HalModule extends SimpleModule {
continue;
}
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
LinkRelation rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
if (!link.hasRel(rel)) {
curiedLinkPresent = true;
}
sortedLinks //
.computeIfAbsent(rel, key -> new ArrayList<>())//
.add(toHalLink(link));
.computeIfAbsent(rel, key -> new ArrayList<>())//
.add(toHalLink(link));
links.add(link);
}
@@ -185,18 +183,19 @@ public class Jackson2HalModule extends SimpleModule {
if (!skipCuries && prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
curies.add(curieProvider.getCurieInformation(Links.of(links)));
sortedLinks.put("curies", curies);
sortedLinks.put(HalLinkRelation.CURIES, curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.constructType(String.class);
JavaType keyType = typeFactory.constructType(LinkRelation.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(Collections.emptySet(), mapType, true, null,
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration), null);
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, halConfiguration),
null);
serializer.serialize(sortedLinks, jgen, provider);
}
@@ -209,29 +208,24 @@ public class Jackson2HalModule extends SimpleModule {
*/
private HalLink toHalLink(Link link) {
String rel = link.getRel();
String title = getTitle(rel);
HalLinkRelation rel = HalLinkRelation.of(link.getRel());
if (title == null) {
title = getTitle(rel.contains(":") ? rel.substring(rel.indexOf(":") + 1) : rel);
}
return new HalLink(link, title);
return new HalLink(link, getTitle(rel));
}
/**
* Returns the title for the given local link relation resolved through the configured {@link MessageSourceAccessor}
* .
*
* @param localRel must not be {@literal null} or empty.
* @param relation must not be {@literal null} or empty.
* @return
*/
private String getTitle(String localRel) {
private String getTitle(HalLinkRelation relation) {
Assert.hasText(localRel, "Local relation must not be null or empty!");
Assert.notNull(relation, "Local relation must not be null or empty!");
try {
return accessor == null ? null : accessor.getMessage(String.format(RELATION_MESSAGE_TEMPLATE, localRel));
return accessor == null ? null : accessor.getMessage(relation);
} catch (NoSuchMessageException o_O) {
return null;
}
@@ -243,7 +237,7 @@ public class Jackson2HalModule extends SimpleModule {
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
throws JsonMappingException {
return new HalLinkListSerializer(property, curieProvider, mapper, accessor, halConfiguration);
}
@@ -269,7 +263,8 @@ public class Jackson2HalModule extends SimpleModule {
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object)
*/
public boolean isEmpty(SerializerProvider provider, List<Link> value) {
@Override
public boolean isEmpty(SerializerProvider provider, Links value) {
return value.isEmpty();
}
@@ -278,8 +273,8 @@ public class Jackson2HalModule extends SimpleModule {
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
public boolean hasSingleElement(List<Link> value) {
return value.size() == 1;
public boolean hasSingleElement(Links value) {
return value.toList().size() == 1;
}
/*
@@ -299,7 +294,7 @@ public class Jackson2HalModule extends SimpleModule {
* @author Oliver Gierke
*/
public static class HalResourcesSerializer extends ContainerSerializer<Collection<?>>
implements ContextualSerializer {
implements ContextualSerializer {
private static final long serialVersionUID = 8030706944344625390L;
@@ -325,10 +320,9 @@ public class Jackson2HalModule extends SimpleModule {
* org.codehaus.jackson.map.SerializerProvider)
*/
@Override
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
public void serialize(Collection<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
Map<String, Object> embeddeds = embeddedMapper.map(value);
Map<HalLinkRelation, Object> embeddeds = embeddedMapper.map(value);
Object currentValue = jgen.getCurrentValue();
@@ -344,7 +338,7 @@ public class Jackson2HalModule extends SimpleModule {
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
throws JsonMappingException {
return new HalResourcesSerializer(property, embeddedMapper);
}
@@ -358,6 +352,7 @@ public class Jackson2HalModule extends SimpleModule {
return null;
}
@Override
public boolean isEmpty(SerializerProvider provider, Collection<?> value) {
return value.isEmpty();
}
@@ -381,7 +376,7 @@ public class Jackson2HalModule extends SimpleModule {
* @author Oliver Gierke
*/
public static class OptionalListJackson2Serializer extends ContainerSerializer<Object>
implements ContextualSerializer {
implements ContextualSerializer {
private static final long serialVersionUID = 3700806118177419817L;
@@ -436,7 +431,7 @@ public class Jackson2HalModule extends SimpleModule {
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
throws IOException {
while (value.hasNext()) {
Object elem = value.next();
@@ -449,7 +444,7 @@ public class Jackson2HalModule extends SimpleModule {
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
throws JsonMappingException {
throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
@@ -493,6 +488,7 @@ public class Jackson2HalModule extends SimpleModule {
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object)
*/
@Override
public boolean isEmpty(SerializerProvider provider, Object value) {
return false;
}
@@ -505,7 +501,7 @@ public class Jackson2HalModule extends SimpleModule {
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
throws JsonMappingException {
return new OptionalListJackson2Serializer(property, halConfiguration);
}
}
@@ -541,8 +537,7 @@ public class Jackson2HalModule extends SimpleModule {
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public List<Link> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
public List<Link> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
List<Link> result = new ArrayList<>();
String relation;
@@ -561,19 +556,13 @@ public class Jackson2HalModule extends SimpleModule {
if (JsonToken.START_ARRAY.equals(jp.nextToken())) {
while (!JsonToken.END_ARRAY.equals(jp.nextToken())) {
link = jp.readValueAs(Link.class);
result.add(new Link(link.getHref(), relation)
.withHreflang(link.getHreflang())
.withTitle(link.getTitle())
.withType(link.getType())
.withDeprecation(link.getDeprecation()));
result.add(new Link(link.getHref(), relation).withHreflang(link.getHreflang()).withTitle(link.getTitle())
.withType(link.getType()).withDeprecation(link.getDeprecation()));
}
} else {
link = jp.readValueAs(Link.class);
result.add(new Link(link.getHref(), relation)
.withHreflang(link.getHreflang())
.withTitle(link.getTitle())
.withType(link.getType())
.withDeprecation(link.getDeprecation()));
result.add(new Link(link.getHref(), relation).withHreflang(link.getHreflang()).withTitle(link.getTitle())
.withType(link.getType()).withDeprecation(link.getDeprecation()));
}
}
@@ -582,7 +571,7 @@ public class Jackson2HalModule extends SimpleModule {
}
public static class HalResourcesDeserializer extends ContainerDeserializerBase<List<Object>>
implements ContextualDeserializer {
implements ContextualDeserializer {
private static final long serialVersionUID = 4755806754621032622L;
@@ -625,8 +614,7 @@ public class Jackson2HalModule extends SimpleModule {
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
public List<Object> deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
List<Object> result = new ArrayList<>();
JsonDeserializer<Object> deser = ctxt.findRootValueDeserializer(contentType);
@@ -655,7 +643,7 @@ public class Jackson2HalModule extends SimpleModule {
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
throws JsonMappingException {
JavaType vc = property.getType().getContentType();
HalResourcesDeserializer des = new HalResourcesDeserializer(vc);
@@ -674,7 +662,7 @@ public class Jackson2HalModule extends SimpleModule {
private final AutowireCapableBeanFactory delegate;
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
MessageSourceAccessor messageSourceAccessor) {
MessageSourceAccessor messageSourceAccessor) {
this(provider, curieProvider, messageSourceAccessor, new HalConfiguration());
}
@@ -688,7 +676,7 @@ public class Jackson2HalModule extends SimpleModule {
* @param messageSourceAccessor can be {@literal null}.
*/
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider,
MessageSourceAccessor messageSourceAccessor, HalConfiguration halConfiguration) {
MessageSourceAccessor messageSourceAccessor, HalConfiguration halConfiguration) {
this(provider, curieProvider, messageSourceAccessor, true, halConfiguration);
}
@@ -704,12 +692,12 @@ public class Jackson2HalModule extends SimpleModule {
* @param enforceEmbeddedCollections
*/
public HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor,
boolean enforceEmbeddedCollections, HalConfiguration halConfiguration) {
boolean enforceEmbeddedCollections, HalConfiguration halConfiguration) {
this(provider, curieProvider, accessor, enforceEmbeddedCollections, null, halConfiguration);
}
private HalHandlerInstantiator(RelProvider provider, CurieProvider curieProvider, MessageSourceAccessor accessor,
boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate, HalConfiguration halConfiguration) {
boolean enforceEmbeddedCollections, AutowireCapableBeanFactory delegate, HalConfiguration halConfiguration) {
Assert.notNull(provider, "RelProvider must not be null!");
@@ -719,7 +707,7 @@ public class Jackson2HalModule extends SimpleModule {
this.serializers.put(HalResourcesSerializer.class, new HalResourcesSerializer(mapper));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, accessor, halConfiguration));
new HalLinkListSerializer(curieProvider, mapper, accessor, halConfiguration));
}
/*
@@ -728,7 +716,7 @@ public class Jackson2HalModule extends SimpleModule {
*/
@Override
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> deserClass) {
Class<?> deserClass) {
return (JsonDeserializer<?>) findInstance(deserClass);
}
@@ -738,7 +726,7 @@ public class Jackson2HalModule extends SimpleModule {
*/
@Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> keyDeserClass) {
Class<?> keyDeserClass) {
return (KeyDeserializer) findInstance(keyDeserClass);
}
@@ -757,7 +745,7 @@ public class Jackson2HalModule extends SimpleModule {
*/
@Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated,
Class<?> builderClass) {
Class<?> builderClass) {
return (TypeResolverBuilder<?>) findInstance(builderClass);
}
@@ -796,6 +784,7 @@ public class Jackson2HalModule extends SimpleModule {
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonSerializer#isEmpty(com.fasterxml.jackson.databind.SerializerProvider, java.lang.Object)
*/
@Override
public boolean isEmpty(SerializerProvider provider, Boolean value) {
return value == null || Boolean.FALSE.equals(value);
}
@@ -805,8 +794,7 @@ public class Jackson2HalModule extends SimpleModule {
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(Boolean value, JsonGenerator jgen, SerializerProvider provider)
throws IOException {
public void serialize(Boolean value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeBoolean(value);
}
@@ -825,7 +813,7 @@ public class Jackson2HalModule extends SimpleModule {
*/
@Override
public void acceptJsonFormatVisitor(JsonFormatVisitorWrapper visitor, JavaType typeHint)
throws JsonMappingException {
throws JsonMappingException {
if (visitor != null) {
visitor.expectBooleanFormat(typeHint);
}
@@ -866,7 +854,7 @@ public class Jackson2HalModule extends SimpleModule {
* @param source must not be {@literal null}.
* @return
*/
public Map<String, Object> map(Iterable<?> source) {
public Map<HalLinkRelation, Object> map(Iterable<?> source) {
Assert.notNull(source, "Elements must not be null!");
@@ -888,7 +876,7 @@ public class Jackson2HalModule extends SimpleModule {
public boolean hasCuriedEmbed(Iterable<?> source) {
return map(source).keySet().stream() //
.anyMatch(rel -> rel.contains(":"));
.anyMatch(HalLinkRelation::isCuried);
}
}

View File

@@ -25,7 +25,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Custom mixin to avoid rel attributes being rendered for HAL.
*
*
* @author Alexander Baetz
* @author Oliver Gierke
* @author Greg Turnquist

View File

@@ -15,9 +15,8 @@
*/
package org.springframework.hateoas.hal;
import java.util.List;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -40,5 +39,5 @@ public abstract class ResourceSupportMixin extends ResourceSupport {
@JsonInclude(Include.NON_EMPTY)
@JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class)
@JsonDeserialize(using = Jackson2HalModule.HalLinkListDeserializer.class)
public abstract List<Link> getLinks();
public abstract Links getLinks();
}

View File

@@ -34,7 +34,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
* @author Greg Turnquist
*/
@JsonPropertyOrder({ "content", "links" })
public abstract class ResourcesMixin<T> extends Resources<T> {
abstract class ResourcesMixin<T> extends Resources<T> {
@Override
@JsonProperty("_embedded")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 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.
@@ -15,10 +15,11 @@
*/
package org.springframework.hateoas.hal.forms;
import static org.springframework.http.HttpMethod.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
@@ -36,39 +37,40 @@ import org.springframework.http.MediaType;
/**
* {@link AffordanceModel} for a HAL-FORMS {@link MediaType}.
*
*
* @author Greg Turnquist
* @author Oliver Gierke
*/
@EqualsAndHashCode(callSuper = true)
public class HalFormsAffordanceModel extends AffordanceModel {
class HalFormsAffordanceModel extends AffordanceModel {
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH);
private static final Set<HttpMethod> ENTITY_ALTERING_METHODS = EnumSet.of(POST, PUT, PATCH);
private static final Set<HttpMethod> REQUIRED_METHODS = EnumSet.of(POST, PUT);
private final @Getter List<HalFormsProperty> inputProperties;
public HalFormsAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
public HalFormsAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
super(name, link, httpMethod, inputType, queryMethodParameters, outputType);
this.inputProperties = determineInputs();
}
/**
* Look at the input's domain type to extract the {@link Affordance}'s properties.
* Then transform them into a list of {@link HalFormsProperty} objects.
* Look at the input's domain type to extract the {@link Affordance}'s properties. Then transform them into a list of
* {@link HalFormsProperty} objects.
*/
private List<HalFormsProperty> determineInputs() {
if (ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
return PropertyUtils.findPropertyNames(getInputType()).stream()
.map(propertyName -> new HalFormsProperty()
.withName(propertyName)
.withRequired(Arrays.asList(HttpMethod.POST, HttpMethod.PUT).contains(getHttpMethod())))
.collect(Collectors.toList());
} else {
if (!ENTITY_ALTERING_METHODS.contains(getHttpMethod())) {
return Collections.emptyList();
}
return PropertyUtils.findPropertyNames(getInputType()).stream() //
.map(propertyName -> new HalFormsProperty() //
.withName(propertyName) //
.withRequired(REQUIRED_METHODS.contains(getHttpMethod()))) //
.collect(Collectors.toList());
}
}

View File

@@ -38,8 +38,13 @@ class HalFormsAffordanceModelFactory implements AffordanceModelFactory {
private final @Getter MediaType mediaType = MediaTypes.HAL_FORMS_JSON;
/*
* (non-Javadoc)
* @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(java.lang.String, org.springframework.hateoas.Link, org.springframework.http.HttpMethod, org.springframework.core.ResolvableType, java.util.List, org.springframework.core.ResolvableType)
*/
@Override
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
return new HalFormsAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType);
}
}

View File

@@ -21,17 +21,17 @@ import lombok.Singular;
import lombok.Value;
import lombok.experimental.Wither;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer;
import org.springframework.hateoas.hal.HalLinkRelation;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.hateoas.hal.forms.Jackson2HalFormsModule.HalFormsLinksDeserializer;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -45,7 +45,7 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Representation of a HAL-FORMS document.
*
*
* @author Dietrich Schulten
* @author Greg Turnquist
* @author Oliver Gierke
@@ -67,7 +67,7 @@ public class HalFormsDocument<T> {
@JsonProperty("_embedded") //
@JsonInclude(Include.NON_EMPTY) //
private Map<String, Object> embedded;
private Map<HalLinkRelation, Object> embedded;
@JsonProperty("page") //
@JsonInclude(Include.NON_NULL) //
@@ -77,8 +77,8 @@ public class HalFormsDocument<T> {
@JsonProperty("_links") //
@JsonInclude(Include.NON_EMPTY) //
@JsonSerialize(using = HalLinkListSerializer.class) //
@JsonDeserialize(using = HalLinkListDeserializer.class) //
private List<Link> links;
@JsonDeserialize(using = HalFormsLinksDeserializer.class) //
private Links links;
@Singular //
@JsonProperty("_templates") //
@@ -86,12 +86,12 @@ public class HalFormsDocument<T> {
private Map<String, HalFormsTemplate> templates;
private HalFormsDocument() {
this(null, null, Collections.emptyMap(), null, Collections.emptyList(), Collections.emptyMap());
this(null, null, Collections.emptyMap(), null, Links.NONE, Collections.emptyMap());
}
/**
* Creates a new {@link HalFormsDocument} for the given resource.
*
*
* @param resource can be {@literal null}.
* @return
*/
@@ -101,7 +101,7 @@ public class HalFormsDocument<T> {
/**
* returns a new {@link HalFormsDocument} for the given resources.
*
*
* @param resources must not be {@literal null}.
* @return
*/
@@ -114,7 +114,7 @@ public class HalFormsDocument<T> {
/**
* Creates a new empty {@link HalFormsDocument}.
*
*
* @return
*/
public static HalFormsDocument<?> empty() {
@@ -123,7 +123,7 @@ public class HalFormsDocument<T> {
/**
* Returns the default template of the document.
*
*
* @return
*/
@JsonIgnore
@@ -133,7 +133,7 @@ public class HalFormsDocument<T> {
/**
* Returns the template with the given name.
*
*
* @param key must not be {@literal null}.
* @return
*/
@@ -147,7 +147,7 @@ public class HalFormsDocument<T> {
/**
* Adds the given {@link Link} to the current document.
*
*
* @param link must not be {@literal null}.
* @return
*/
@@ -155,15 +155,12 @@ public class HalFormsDocument<T> {
Assert.notNull(link, "Link must not be null!");
List<Link> links = new ArrayList<>(this.links);
links.add(link);
return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links, templates);
return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links.and(link), templates);
}
/**
* Adds the given {@link HalFormsTemplate} to the current document.
*
*
* @param name must not be {@literal null} or empty.
* @param template must not be {@literal null}.
* @return
@@ -181,19 +178,23 @@ public class HalFormsDocument<T> {
/**
* Adds the given value as embedded one.
*
*
* @param key must not be {@literal null} or empty.
* @param value must not be {@literal null}.
* @return
*/
public HalFormsDocument<T> andEmbedded(String key, Object value) {
public HalFormsDocument<T> andEmbedded(HalLinkRelation key, Object value) {
Assert.notNull(key, "Embedded key must not be null!");
Assert.notNull(value, "Embedded value must not be null!");
Map<String, Object> embedded = new HashMap<>(this.embedded);
Map<HalLinkRelation, Object> embedded = new HashMap<>(this.embedded);
embedded.put(key, value);
return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links, templates);
}
HalFormsDocument<T> withLinks(Links links) {
return new HalFormsDocument<>(resource, resources, embedded, pageMetadata, links, templates);
}
}

View File

@@ -15,41 +15,19 @@
*/
package org.springframework.hateoas.hal.forms;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.core.JsonPathLinkDiscoverer;
import org.springframework.hateoas.hal.HalLinkDiscoverer;
/**
* HAL-FORMS based {@link JsonPathLinkDiscoverer}.
*
*
* @author Greg Turnquist
* @author Oliver Gierke
*/
public class HalFormsLinkDiscoverer extends JsonPathLinkDiscoverer {
public class HalFormsLinkDiscoverer extends HalLinkDiscoverer {
public HalFormsLinkDiscoverer() {
super("$._links..['%s']", MediaTypes.HAL_FORMS_JSON);
}
@Override
@SuppressWarnings("unchecked")
protected Link extractLink(Object element, String rel) {
if (element instanceof Map) {
Map<String, String> json = (Map<String, String>) element;
return new Link(json.get("href"), rel)
.withHreflang(json.get("hreflang"))
.withMedia(json.get("media"))
.withTitle(json.get("title"))
.withType(json.get("type"))
.withDeprecation(json.get("deprecation"))
.withProfile(json.get("profile"))
.withName(json.get("name"));
}
return super.extractLink(element, rel);
super(MediaTypes.HAL_FORMS_JSON);
}
}

View File

@@ -28,7 +28,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
/**
* Describe a parameter for the associated state transition in a HAL-FORMS document. A {@link HalFormsTemplate} may
* contain a list of {@link HalFormsProperty}s
*
*
* @see http://mamund.site44.com/misc/hal-forms/
*/
@JsonInclude(Include.NON_DEFAULT)
@@ -36,7 +36,7 @@ import com.fasterxml.jackson.annotation.JsonInclude.Include;
@Wither
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@NoArgsConstructor(force = true)
class HalFormsProperty {
public class HalFormsProperty {
private @NonNull String name;
private Boolean readOnly;
@@ -49,7 +49,7 @@ class HalFormsProperty {
/**
* Creates a new {@link HalFormsProperty} with the given name.
*
*
* @param name must not be {@literal null}.
* @return
*/

View File

@@ -18,16 +18,18 @@ package org.springframework.hateoas.hal.forms;
import java.io.IOException;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.IanaLinkRelation;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.hal.HalLinkRelation;
import org.springframework.hateoas.hal.Jackson2HalModule;
import org.springframework.http.HttpMethod;
@@ -74,9 +76,7 @@ class HalFormsSerializers {
.withLinks(value.getLinks()) //
.withTemplates(findTemplates(value));
provider
.findValueSerializer(HalFormsDocument.class, property)
.serialize(doc, gen, provider);
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
@Override
@@ -131,7 +131,7 @@ class HalFormsSerializers {
@Override
public void serialize(Resources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
Map<String, Object> embeddeds = embeddedMapper.map(value);
Map<HalLinkRelation, Object> embeddeds = embeddedMapper.map(value);
HalFormsDocument<?> doc;
@@ -151,9 +151,7 @@ class HalFormsSerializers {
.withTemplates(findTemplates(value));
}
provider
.findValueSerializer(HalFormsDocument.class, property)
.serialize(doc, gen, provider);
provider.findValueSerializer(HalFormsDocument.class, property).serialize(doc, gen, provider);
}
@Override
@@ -191,30 +189,29 @@ class HalFormsSerializers {
*/
private static Map<String, HalFormsTemplate> findTemplates(ResourceSupport resource) {
if (!resource.hasLink(IanaLinkRelations.SELF)) {
return Collections.emptyMap();
}
Map<String, HalFormsTemplate> templates = new HashMap<>();
List<Affordance> affordances = resource.getLink(IanaLinkRelations.SELF).map(Link::getAffordances)
.orElse(Collections.emptyList());
if (resource.hasLink(IanaLinkRelation.SELF.value())) {
affordances.stream() //
.map(it -> it.getAffordanceModel(MediaTypes.HAL_FORMS_JSON)) //
.map(HalFormsAffordanceModel.class::cast) //
.filter(it -> !it.hasHttpMethod(HttpMethod.GET)) //
.peek(it -> validate(resource, it)) //
.forEach(it -> {
for (Affordance affordance : resource.getLink(IanaLinkRelation.SELF.value()).map(Link::getAffordances)
.orElse(Collections.emptyList())) {
HalFormsAffordanceModel model = affordance.getAffordanceModel(MediaTypes.HAL_FORMS_JSON);
if (!(model.getHttpMethod() == HttpMethod.GET)) {
validate(resource, model);
HalFormsTemplate template = HalFormsTemplate.forMethod(model.getHttpMethod()) //
.withProperties(model.getInputProperties());
HalFormsTemplate template = HalFormsTemplate.forMethod(it.getHttpMethod()) //
.withProperties(it.getInputProperties());
/*
* First template in HAL-FORMS is "default".
*/
templates.put(templates.isEmpty() ? "default" : model.getName(), template);
}
}
}
templates.put(templates.isEmpty() ? "default" : it.getName(), template);
});
return templates;
}
@@ -228,11 +225,11 @@ class HalFormsSerializers {
private static void validate(ResourceSupport resource, HalFormsAffordanceModel model) {
String affordanceUri = model.getURI();
String selfLinkUri = resource.getRequiredLink(IanaLinkRelation.SELF.value()).expand().getHref();
String selfLinkUri = resource.getRequiredLink(IanaLinkRelations.SELF.value()).expand().getHref();
if (!affordanceUri.equals(selfLinkUri)) {
throw new IllegalStateException("Affordance's URI " + affordanceUri + " doesn't match self link "
+ selfLinkUri + " as expected in HAL-FORMS");
throw new IllegalStateException("Affordance's URI " + affordanceUri + " doesn't match self link " + selfLinkUri
+ " as expected in HAL-FORMS");
}
}
}

View File

@@ -15,6 +15,7 @@
*/
package org.springframework.hateoas.hal.forms;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
@@ -22,6 +23,7 @@ import java.util.Map;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
@@ -29,6 +31,7 @@ import org.springframework.hateoas.Resources;
import org.springframework.hateoas.hal.CurieProvider;
import org.springframework.hateoas.hal.Jackson2HalModule.EmbeddedMapper;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListDeserializer;
import org.springframework.hateoas.hal.Jackson2HalModule.HalLinkListSerializer;
import org.springframework.hateoas.hal.LinkMixin;
import org.springframework.hateoas.hal.ResourceSupportMixin;
@@ -41,8 +44,11 @@ import org.springframework.http.MediaType;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.KeyDeserializer;
@@ -50,11 +56,13 @@ import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
/**
* Serialize / deserialize all the parts of HAL-FORMS documents using Jackson.
@@ -78,7 +86,6 @@ public class Jackson2HalFormsModule extends SimpleModule {
setMixInAnnotation(MediaType.class, MediaTypeMixin.class);
addSerializer(new HalFormsResourceSerializer());
}
@JsonSerialize(using = HalFormsResourceSerializer.class)
@@ -108,6 +115,35 @@ public class Jackson2HalFormsModule extends SimpleModule {
@JsonDeserialize(using = MediaTypeDeserializer.class)
interface MediaTypeMixin {}
static class HalFormsLinksDeserializer extends ContainerDeserializerBase<Links> {
private static final long serialVersionUID = -848240531474910385L;
private final HalLinkListDeserializer delegate = new HalLinkListDeserializer();
public HalFormsLinksDeserializer() {
super(TypeFactory.defaultInstance().constructType(Links.class));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentDeserializer()
*/
@Override
public JsonDeserializer<Object> getContentDeserializer() {
return delegate.getContentDeserializer();
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public Links deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
return Links.of(delegate.deserialize(p, ctxt));
}
}
/**
* Create new HAL-FORMS serializers based on the context.
*/
@@ -116,22 +152,24 @@ public class Jackson2HalFormsModule extends SimpleModule {
private final Map<Class<?>, Object> serializers = new HashMap<>();
public HalFormsHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider,
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections,
HalFormsConfiguration halFormsConfiguration) {
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections,
HalFormsConfiguration halFormsConfiguration) {
super(resolver, curieProvider, messageSource, enforceEmbeddedCollections, halFormsConfiguration.toHalConfiguration());
super(resolver, curieProvider, messageSource, enforceEmbeddedCollections,
halFormsConfiguration.toHalConfiguration());
EmbeddedMapper mapper = new EmbeddedMapper(resolver, curieProvider, enforceEmbeddedCollections);
this.serializers.put(HalFormsResourcesSerializer.class, new HalFormsResourcesSerializer(mapper));
this.serializers.put(HalLinkListSerializer.class,
new HalLinkListSerializer(curieProvider, mapper, messageSource, halFormsConfiguration.toHalConfiguration()));
new HalLinkListSerializer(curieProvider, mapper, messageSource, halFormsConfiguration.toHalConfiguration()));
}
public HalFormsHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider,
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections,
AutowireCapableBeanFactory beanFactory) {
this(relProvider, curieProvider, messageSource, enforceEmbeddedCollections, beanFactory.getBean(HalFormsConfiguration.class));
MessageSourceAccessor messageSource, boolean enforceEmbeddedCollections,
AutowireCapableBeanFactory beanFactory) {
this(relProvider, curieProvider, messageSource, enforceEmbeddedCollections,
beanFactory.getBean(HalFormsConfiguration.class));
}
private Object findInstance(Class<?> type) {

View File

@@ -17,6 +17,7 @@ package org.springframework.hateoas.mvc;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.ExposesResourceFor;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.RelProvider;
import org.springframework.plugin.core.PluginRegistry;
import org.springframework.util.Assert;
@@ -42,25 +43,25 @@ public class ControllerRelProvider implements RelProvider {
this.providers = providers;
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.RelProvider#getRelForCollectionResource(java.lang.Class)
*/
@Override
public String getCollectionResourceRelFor(Class<?> resource) {
public LinkRelation getCollectionResourceRelFor(Class<?> resource) {
return providers.getRequiredPluginFor(entityType).getCollectionResourceRelFor(resource);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.hateoas.RelProvider#getRelForSingleResource(java.lang.Class)
*/
@Override
public String getItemResourceRelFor(Class<?> resource) {
public LinkRelation getItemResourceRelFor(Class<?> resource) {
return providers.getRequiredPluginFor(entityType).getItemResourceRelFor(resource);
}
/*
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.hateoas.mvc;
import java.util.List;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.ResourceProcessor;
@@ -33,14 +31,14 @@ import org.springframework.util.Assert;
* support code that will transparently invoke the header exposure. If you use this class from a controller directly,
* the {@link Link}s will not be present in the {@link ResourceSupport} instance anymore when {@link ResourceProcessor}s
* kick in.
*
*
* @author Oliver Gierke
*/
public class HeaderLinksResponseEntity<T extends ResourceSupport> extends ResponseEntity<T> {
/**
* Creates a new {@link HeaderLinksResponseEntity} from the given {@link ResponseEntity}.
*
*
* @param entity must not be {@literal null}.
*/
private HeaderLinksResponseEntity(ResponseEntity<T> entity) {
@@ -52,7 +50,7 @@ public class HeaderLinksResponseEntity<T extends ResourceSupport> extends Respon
/**
* Creates a new {@link HeaderLinksResponseEntity} from the given {@link HttpEntity} by defaulting the status code to
* {@link HttpStatus#OK}.
*
*
* @param entity must not be {@literal null}.
*/
private HeaderLinksResponseEntity(HttpEntity<T> entity) {
@@ -62,7 +60,7 @@ public class HeaderLinksResponseEntity<T extends ResourceSupport> extends Respon
/**
* Wraps the given {@link HttpEntity} into a {@link HeaderLinksResponseEntity}. Will default the status code to
* {@link HttpStatus#OK} if the given value is not a {@link ResponseEntity}.
*
*
* @param entity must not be {@literal null}.
* @return
*/
@@ -80,7 +78,7 @@ public class HeaderLinksResponseEntity<T extends ResourceSupport> extends Respon
/**
* Wraps the given {@link ResourceSupport} into a {@link HeaderLinksResponseEntity}. Will default the status code to
* {@link HttpStatus#OK}.
*
*
* @param entity must not be {@literal null}.
* @return
*/
@@ -94,17 +92,17 @@ public class HeaderLinksResponseEntity<T extends ResourceSupport> extends Respon
/**
* Returns the {@link Link}s contained in the {@link ResourceSupport} of the given {@link ResponseEntity} as
* {@link HttpHeaders}.
*
*
* @param entity must not be {@literal null}.
* @return
*/
private static <T extends ResourceSupport> HttpHeaders getHeadersWithLinks(ResponseEntity<T> entity) {
List<Link> links = entity.getBody().getLinks();
Links links = entity.getBody().getLinks();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(entity.getHeaders());
httpHeaders.add("Link", new Links(links).toString());
httpHeaders.add("Link", links.toString());
return httpHeaders;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2017 the original author or authors.
* Copyright 2017-2019 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.
@@ -20,14 +20,13 @@ import java.io.IOException;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
/**
* Simple Jackson serializers and deserializers.
*
*
* @author Oliver Gierke
*/
public class JacksonSerializers {
@@ -46,13 +45,12 @@ public class JacksonSerializers {
super(MediaType.class);
}
/*
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public MediaType deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
public MediaType deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return MediaType.parseMediaType(p.getText());
}
}

View File

@@ -19,17 +19,18 @@ import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.springframework.beans.BeanUtils;
import org.springframework.hateoas.ResourceAssembler;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.core.Objects;
import org.springframework.util.Assert;
/**
* Base class to implement {@link ResourceAssembler}s. Will automate {@link ResourceSupport} instance creation and make
* sure a self-link is always added.
*
*
* @author Oliver Gierke
* @author Greg Turnquist
*/
@@ -40,19 +41,23 @@ public abstract class ResourceAssemblerSupport<T, D extends ResourceSupport> imp
/**
* Creates a new {@link ResourceAssemblerSupport} using the given controller class and resource type.
*
*
* @param controllerClass must not be {@literal null}.
* @param resourceType must not be {@literal null}.
*/
public ResourceAssemblerSupport(Class<?> controllerClass, Class<D> resourceType) {
Objects.requireNonNull(controllerClass, "ControllerClass must not be null!");
Objects.requireNonNull(resourceType, "ResourceType must not be null!");
Assert.notNull(controllerClass, "ControllerClass must not be null!");
Assert.notNull(resourceType, "ResourceType must not be null!");
this.controllerClass = controllerClass;
this.resourceType = resourceType;
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.ResourceAssembler#toResources(java.lang.Iterable)
*/
@Override
public Resources<D> toResources(Iterable<? extends T> entities) {
return this.map(entities).toResources();
@@ -64,7 +69,7 @@ public abstract class ResourceAssemblerSupport<T, D extends ResourceSupport> imp
/**
* Creates a new resource with a self link to the given id.
*
*
* @param entity must not be {@literal null}.
* @param id must not be {@literal null}.
* @return
@@ -75,8 +80,8 @@ public abstract class ResourceAssemblerSupport<T, D extends ResourceSupport> imp
protected D createResourceWithId(Object id, T entity, Object... parameters) {
Objects.requireNonNull(entity, "Entity must not be null!");
Objects.requireNonNull(id, "Id must not be null!");
Assert.notNull(entity, "Entity must not be null!");
Assert.notNull(id, "Id must not be null!");
D instance = instantiateResource(entity);
instance.add(linkTo(this.controllerClass, parameters).slash(id).withSelfRel());
@@ -87,7 +92,7 @@ public abstract class ResourceAssemblerSupport<T, D extends ResourceSupport> imp
* Instantiates the resource object. Default implementation will assume a no-arg constructor and use reflection but
* can be overridden to manually set up the object instance initially (e.g. to improve performance if this becomes an
* issue).
*
*
* @param entity
* @return
*/
@@ -110,7 +115,6 @@ public abstract class ResourceAssemblerSupport<T, D extends ResourceSupport> imp
* Transform a list of {@code T}s into a list of {@link ResourceSupport}s.
*
* @see #toListOfResources() if you need this transformed list rendered as hypermedia
*
* @return
*/
public List<D> toListOfResources() {

View File

@@ -24,6 +24,7 @@ import org.springframework.core.ResolvableType;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.AffordanceModelFactory;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.QueryParameter;
import org.springframework.hateoas.core.DummyInvocationUtils.MethodInvocation;
import org.springframework.hateoas.core.MappingDiscoverer;
@@ -35,7 +36,7 @@ import org.springframework.web.util.UriComponents;
/**
* Extract information needed to assemble an {@link Affordance} from a Spring MVC web method.
*
*
* @author Greg Turnquist
*/
class SpringMvcAffordanceBuilder {
@@ -43,7 +44,7 @@ class SpringMvcAffordanceBuilder {
/**
* Use the attributes of the current method call along with a collection of {@link AffordanceModelFactory}'s to create
* a set of {@link Affordance}s.
*
*
* @param invocation
* @param discoverer
* @param components
@@ -57,24 +58,24 @@ class SpringMvcAffordanceBuilder {
for (HttpMethod requestMethod : discoverer.getRequestMethod(invocation.getTargetType(), invocation.getMethod())) {
String methodName = invocation.getMethod().getName();
Link affordanceLink = new Link(components.toUriString()).withRel(methodName);
Link affordanceLink = new Link(components.toUriString()).withRel(LinkRelation.of(methodName));
MethodParameters invocationMethodParameters = new MethodParameters(invocation.getMethod());
ResolvableType inputType = invocationMethodParameters.getParametersWith(RequestBody.class).stream()
.findFirst()
.map(ResolvableType::forMethodParameter)
.orElse(ResolvableType.NONE);
List<QueryParameter> queryMethodParameters = invocationMethodParameters.getParametersWith(RequestParam.class).stream()
.map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class))
.map(requestParam -> new QueryParameter(requestParam.name(), requestParam.value(), requestParam.required()))
.collect(Collectors.toList());
ResolvableType inputType = invocationMethodParameters.getParametersWith(RequestBody.class).stream() //
.findFirst() //
.map(ResolvableType::forMethodParameter) //
.orElse(ResolvableType.NONE);
List<QueryParameter> queryMethodParameters = invocationMethodParameters.getParametersWith(RequestParam.class)
.stream() //
.map(methodParameter -> methodParameter.getParameterAnnotation(RequestParam.class)) //
.map(requestParam -> new QueryParameter(requestParam.name(), requestParam.value(), requestParam.required())) //
.collect(Collectors.toList());
ResolvableType outputType = ResolvableType.forMethodReturnType(invocation.getMethod());
affordances.add(new Affordance(methodName, affordanceLink, requestMethod, inputType, queryMethodParameters, outputType));
affordances
.add(new Affordance(methodName, affordanceLink, requestMethod, inputType, queryMethodParameters, outputType));
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2015-2019 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.
@@ -18,7 +18,6 @@ package org.springframework.hateoas.mvc;
import java.io.Serializable;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.HashMap;
import org.springframework.core.GenericTypeResolver;
@@ -27,8 +26,8 @@ import org.springframework.util.Assert;
/**
* Helper to easily create {@link ParameterizedTypeReference} instances to Spring HATEOAS resource types. They're
* basically a shortcut over using a verbose {@code new ParameterizedTypeReference<Resources<DomainType>>() .
*
* basically a shortcut over using a verbose {@code new ParameterizedTypeReference<Resources<DomainType>>()}.
*
* @author Oliver Gierke
* @since 0.17
*/
@@ -40,8 +39,8 @@ public class TypeReferences {
* @author Oliver Gierke
* @since 0.17
*/
public static class ResourceType<T> extends
SyntheticParameterizedTypeReference<org.springframework.hateoas.Resource<T>> {}
public static class ResourceType<T>
extends SyntheticParameterizedTypeReference<org.springframework.hateoas.Resource<T>> {}
/**
* A {@link ParameterizedTypeReference} to return a {@link org.springframework.hateoas.Resources} of some type.
@@ -49,8 +48,8 @@ public class TypeReferences {
* @author Oliver Gierke
* @since 0.17
*/
public static class ResourcesType<T> extends
SyntheticParameterizedTypeReference<org.springframework.hateoas.Resources<T>> {}
public static class ResourcesType<T>
extends SyntheticParameterizedTypeReference<org.springframework.hateoas.Resources<T>> {}
/**
* A {@link ParameterizedTypeReference} to return a {@link org.springframework.hateoas.PagedResources} of some type.
@@ -58,12 +57,12 @@ public class TypeReferences {
* @author Oliver Gierke
* @since 0.17
*/
public static class PagedResourcesType<T> extends
SyntheticParameterizedTypeReference<org.springframework.hateoas.PagedResources<T>> {}
public static class PagedResourcesType<T>
extends SyntheticParameterizedTypeReference<org.springframework.hateoas.PagedResources<T>> {}
/**
* Special {@link ParameterizedTypeReference} to customize the generic type detection and eventually return a synthetic
* {@link ParameterizedType} to represent the resource type along side its generic parameter.
* Special {@link ParameterizedTypeReference} to customize the generic type detection and eventually return a
* synthetic {@link ParameterizedType} to represent the resource type along side its generic parameter.
*
* @author Oliver Gierke
* @since 0.17
@@ -72,7 +71,7 @@ public class TypeReferences {
private final Type type;
@SuppressWarnings({ "rawtypes", "deprecation" })
@SuppressWarnings("rawtypes")
SyntheticParameterizedTypeReference() {
Class<? extends SyntheticParameterizedTypeReference> foo = getClass();
@@ -84,10 +83,12 @@ public class TypeReferences {
Type type = parameterizedTypeReferenceSubclass.getGenericSuperclass();
Assert.isInstanceOf(ParameterizedType.class, type);
ParameterizedType parameterizedType = (ParameterizedType) type;
Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1, String.format("Type must have exactly one generic type argument but has %s.", parameterizedType.getActualTypeArguments().length));
Assert.isTrue(parameterizedType.getActualTypeArguments().length == 1,
String.format("Type must have exactly one generic type argument but has %s.",
parameterizedType.getActualTypeArguments().length));
Class<?> resourceType = GenericTypeResolver.resolveType(parameterizedType.getActualTypeArguments()[0],
new HashMap<>());
new HashMap<>());
this.type = new SyntheticParameterizedType(resourceType, domainType);
}
@@ -96,6 +97,7 @@ public class TypeReferences {
* (non-Javadoc)
* @see org.springframework.core.ParameterizedTypeReference#getType()
*/
@Override
public Type getType() {
return this.type;
}
@@ -106,8 +108,8 @@ public class TypeReferences {
*/
@Override
public boolean equals(Object obj) {
return (this == obj || (obj instanceof SyntheticParameterizedTypeReference && this.type
.equals(((SyntheticParameterizedTypeReference<?>) obj).type)));
return this == obj || obj instanceof SyntheticParameterizedTypeReference
&& this.type.equals(((SyntheticParameterizedTypeReference<?>) obj).type);
}
/*

View File

@@ -15,7 +15,6 @@
*/
package org.springframework.hateoas.uber;
import static org.springframework.hateoas.PagedResources.*;
import static org.springframework.hateoas.support.JacksonHelper.*;
import static org.springframework.hateoas.uber.UberData.*;
@@ -27,9 +26,10 @@ import java.util.Map;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.BeanUtils;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.PagedResources.PageMetadata;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
@@ -40,52 +40,92 @@ import org.springframework.util.StringUtils;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.cfg.HandlerInstantiator;
import com.fasterxml.jackson.databind.cfg.MapperConfig;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
import com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase;
import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.jsontype.TypeIdResolver;
import com.fasterxml.jackson.databind.jsontype.TypeResolverBuilder;
import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.ContainerSerializer;
import com.fasterxml.jackson.databind.ser.ContextualSerializer;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
/**
* Jackson {@link SimpleModule} for {@literal UBER+JSON} serializers and deserializers.
*
*
* @author Greg Turnquist
* @author Jens Schauder
* @since 1.0
*/
public class Jackson2UberModule extends SimpleModule {
private static final long serialVersionUID = -2396790508486870880L;
public Jackson2UberModule() {
super("uber-module", new Version(1, 0, 0, null, "org.springframework.hateoas", "spring-hateoas"));
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
setMixInAnnotation(Resource.class, ResourceMixin.class);
setMixInAnnotation(Resources.class, ResourcesMixin.class);
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
addSerializer(new UberPagedResourcesSerializer());
addSerializer(new UberResourcesSerializer());
addSerializer(new UberResourceSerializer());
addSerializer(new UberResourceSupportSerializer());
setMixInAnnotation(ResourceSupport.class, ResourceSupportMixin.class);
setMixInAnnotation(Resource.class, ResourceMixin.class);
setMixInAnnotation(Resources.class, ResourcesMixin.class);
setMixInAnnotation(PagedResources.class, PagedResourcesMixin.class);
}
/**
* Jackson 2 mixin to handle {@link ResourceSupport} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberResourceSupportDeserializer.class)
abstract class ResourceSupportMixin extends ResourceSupport {}
/**
* Jackson 2 mixin to handle {@link Resource} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberResourceDeserializer.class)
abstract class ResourceMixin<T> extends Resource<T> {}
/**
* Jackson 2 mixin to handle {@link Resources} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberResourcesDeserializer.class)
abstract class ResourcesMixin<T> extends Resources<T> {}
/**
* Jackson 2 mixin to handle {@link PagedResources} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberPagedResourcesDeserializer.class)
abstract class PagedResourcesMixin<T> extends PagedResources<T> {}
/**
* Custom {@link JsonSerializer} to render {@link ResourceSupport} into {@literal UBER+JSON}.
*/
static class UberResourceSupportSerializer extends ContainerSerializer<ResourceSupport>
implements ContextualSerializer {
private static final long serialVersionUID = -572866287910993300L;
private final BeanProperty property;
UberResourceSupportSerializer(BeanProperty property) {
@@ -143,6 +183,8 @@ public class Jackson2UberModule extends SimpleModule {
*/
static class UberResourceSerializer extends ContainerSerializer<Resource<?>> implements ContextualSerializer {
private static final long serialVersionUID = -5538560800604582741L;
private final BeanProperty property;
UberResourceSerializer(BeanProperty property) {
@@ -158,10 +200,9 @@ public class Jackson2UberModule extends SimpleModule {
@Override
public void serialize(Resource<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
UberDocument doc = new UberDocument()
.withUber(new Uber()
.withVersion("1.0")
.withData(extractLinksAndContent(value)));
UberDocument doc = new UberDocument().withUber(new Uber() //
.withVersion("1.0") //
.withData(extractLinksAndContent(value)));
provider //
.findValueSerializer(UberDocument.class, property) //
@@ -200,6 +241,8 @@ public class Jackson2UberModule extends SimpleModule {
*/
static class UberResourcesSerializer extends ContainerSerializer<Resources<?>> implements ContextualSerializer {
private static final long serialVersionUID = 3422019794262694127L;
private BeanProperty property;
UberResourcesSerializer(BeanProperty property) {
@@ -212,6 +255,10 @@ public class Jackson2UberModule extends SimpleModule {
this(null);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(Resources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
@@ -258,6 +305,8 @@ public class Jackson2UberModule extends SimpleModule {
static class UberPagedResourcesSerializer extends ContainerSerializer<PagedResources<?>>
implements ContextualSerializer {
private static final long serialVersionUID = -7892297813593085984L;
private BeanProperty property;
UberPagedResourcesSerializer(BeanProperty property) {
@@ -270,6 +319,10 @@ public class Jackson2UberModule extends SimpleModule {
this(null);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.std.StdSerializer#serialize(java.lang.Object, com.fasterxml.jackson.core.JsonGenerator, com.fasterxml.jackson.databind.SerializerProvider)
*/
@Override
public void serialize(PagedResources<?> value, JsonGenerator gen, SerializerProvider provider) throws IOException {
@@ -283,26 +336,46 @@ public class Jackson2UberModule extends SimpleModule {
.serialize(doc, gen, provider);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentType()
*/
@Override
public JavaType getContentType() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#getContentSerializer()
*/
@Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#hasSingleElement(java.lang.Object)
*/
@Override
public boolean hasSingleElement(PagedResources<?> value) {
return value.getContent().size() == 1;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContainerSerializer#_withValueTypeSerializer(com.fasterxml.jackson.databind.jsontype.TypeSerializer)
*/
@Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
return null;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.ContextualSerializer#createContextual(com.fasterxml.jackson.databind.SerializerProvider, com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property)
throws JsonMappingException {
@@ -310,44 +383,34 @@ public class Jackson2UberModule extends SimpleModule {
}
}
/**
* Custom {@link StdSerializer} to translate {@link UberAction} into the proper JSON representation.
*/
static class UberActionSerializer extends StdSerializer<UberAction> {
UberActionSerializer() {
super(UberAction.class);
}
@Override
public void serialize(UberAction value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString(value.toString());
}
}
/**
* Custom {@link StdDeserializer} to deserialize {@link ResourceSupport}.
*/
static class UberResourceSupportDeserializer extends ContainerDeserializerBase<ResourceSupport>
implements ContextualDeserializer {
private JavaType contentType;
private static final long serialVersionUID = -8738539821441549016L;
private final JavaType contentType;
UberResourceSupportDeserializer(JavaType contentType) {
UberResourceSupportDeserializer() {
this(TypeFactory.defaultInstance().constructType(ResourceSupport.class));
}
private UberResourceSupportDeserializer(JavaType contentType) {
super(contentType);
this.contentType = contentType;
}
UberResourceSupportDeserializer() {
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public ResourceSupport deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
List<Link> links = doc.getUber().getLinks();
Links links = doc.getUber().getLinks();
return doc.getUber().getData().stream() //
.filter(uberData -> !StringUtils.isEmpty(uberData.getName())) //
@@ -363,7 +426,7 @@ public class Jackson2UberModule extends SimpleModule {
}
@NotNull
private ResourceSupport convertToResourceSupport(UberData uberData, List<Link> links) {
private ResourceSupport convertToResourceSupport(UberData uberData, Links links) {
List<UberData> data = uberData.getData();
Map<String, Object> properties;
@@ -382,11 +445,19 @@ public class Jackson2UberModule extends SimpleModule {
return resourceSupport;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return this.contentType;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.ContextualDeserializer#createContextual(com.fasterxml.jackson.databind.DeserializationContext, com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
@@ -414,31 +485,36 @@ public class Jackson2UberModule extends SimpleModule {
static class UberResourceDeserializer extends ContainerDeserializerBase<Resource<?>>
implements ContextualDeserializer {
private JavaType contentType;
private static final long serialVersionUID = 1776321413269082414L;
UberResourceDeserializer(JavaType contentType) {
super(contentType);
this.contentType = contentType;
}
private final JavaType contentType;
UberResourceDeserializer() {
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
}
private UberResourceDeserializer(JavaType contentType) {
super(contentType);
this.contentType = contentType;
}
@Override
public Resource<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
UberDocument doc = p.getCodec().readValue(p, UberDocument.class);
List<Link> links = doc.getUber().getLinks();
Links links = doc.getUber().getLinks();
return doc.getUber().getData().stream().filter(uberData -> !StringUtils.isEmpty(uberData.getName())).findFirst()
.map(uberData -> convertToResource(uberData, links)).orElseThrow(
return doc.getUber().getData().stream() //
.filter(uberData -> !StringUtils.isEmpty(uberData.getName())) //
.findFirst() //
.map(uberData -> convertToResource(uberData, links)) //
.orElseThrow(
() -> new IllegalStateException("No data entry containing a 'value' was found in this document!"));
}
@NotNull
private Resource<Object> convertToResource(UberData uberData, List<Link> links) {
private Resource<Object> convertToResource(UberData uberData, Links links) {
// Primitive type
List<UberData> data = uberData.getData();
@@ -461,11 +537,19 @@ public class Jackson2UberModule extends SimpleModule {
return new Resource<>(value, links);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.std.ContainerDeserializerBase#getContentType()
*/
@Override
public JavaType getContentType() {
return this.contentType;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.deser.ContextualDeserializer#createContextual(com.fasterxml.jackson.databind.DeserializationContext, com.fasterxml.jackson.databind.BeanProperty)
*/
@Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
@@ -493,7 +577,9 @@ public class Jackson2UberModule extends SimpleModule {
static class UberResourcesDeserializer extends ContainerDeserializerBase<Resources<?>>
implements ContextualDeserializer {
private JavaType contentType;
private static final long serialVersionUID = 8722467561709171145L;
private final JavaType contentType;
UberResourcesDeserializer(JavaType contentType) {
@@ -505,6 +591,10 @@ public class Jackson2UberModule extends SimpleModule {
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public Resources<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
@@ -550,6 +640,8 @@ public class Jackson2UberModule extends SimpleModule {
static class UberPagedResourcesDeserializer extends ContainerDeserializerBase<PagedResources<?>>
implements ContextualDeserializer {
private static final long serialVersionUID = 4123359694609188745L;
private JavaType contentType;
UberPagedResourcesDeserializer(JavaType contentType) {
@@ -562,6 +654,10 @@ public class Jackson2UberModule extends SimpleModule {
this(TypeFactory.defaultInstance().constructSimpleType(UberDocument.class, new JavaType[0]));
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public PagedResources<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
@@ -602,12 +698,11 @@ public class Jackson2UberModule extends SimpleModule {
public JsonDeserializer<Object> getContentDeserializer() {
return null;
}
}
/**
* Convert an {@link UberDocument} into a {@link Resources}.
*
*
* @param doc
* @param rootType
* @param contentType
@@ -681,9 +776,7 @@ public class Jackson2UberModule extends SimpleModule {
* ...or return a Resources<T>
*/
List<Object> resourceLessContent = content.stream()
.map(item -> (Resource<?>) item)
.map(Resource::getContent)
List<Object> resourceLessContent = content.stream().map(item -> (Resource<?>) item).map(Resource::getContent)
.collect(Collectors.toList());
return new Resources<>(resourceLessContent, doc.getUber().getLinks());
@@ -697,10 +790,8 @@ public class Jackson2UberModule extends SimpleModule {
private static PageMetadata extractPagingMetadata(UberDocument doc) {
return doc.getUber().getData().stream()
.filter(uberData -> uberData.getName() != null && uberData.getName().equals("page"))
.findFirst()
.map(Jackson2UberModule::convertUberDataToPageMetaData)
.orElse(null);
.filter(uberData -> uberData.getName() != null && uberData.getName().equals("page")).findFirst()
.map(Jackson2UberModule::convertUberDataToPageMetaData).orElse(null);
}
@NotNull
@@ -747,60 +838,19 @@ public class Jackson2UberModule extends SimpleModule {
*/
static class UberActionDeserializer extends StdDeserializer<UberAction> {
private static final long serialVersionUID = -6198451472474285487L;
UberActionDeserializer() {
super(UberAction.class);
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.JsonDeserializer#deserialize(com.fasterxml.jackson.core.JsonParser, com.fasterxml.jackson.databind.DeserializationContext)
*/
@Override
public UberAction deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return UberAction.valueOf(p.getText().toUpperCase());
}
}
public static class UberHandlerInstantiator extends HandlerInstantiator {
private final Map<Class<?>, Object> serializers = new HashMap<>();
public UberHandlerInstantiator() {
this.serializers.put(UberResourceSupportSerializer.class, new UberResourceSupportSerializer());
this.serializers.put(UberResourceSerializer.class, new UberResourceSerializer());
this.serializers.put(UberResourcesSerializer.class, new UberResourcesSerializer());
this.serializers.put(UberPagedResourcesSerializer.class, new UberPagedResourcesSerializer());
}
@Override
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> deserClass) {
return (JsonDeserializer<?>) findInstance(deserClass);
}
@Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated,
Class<?> keyDeserClass) {
return (KeyDeserializer) findInstance(keyDeserClass);
}
@Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
return (JsonSerializer<?>) findInstance(serClass);
}
@Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated,
Class<?> builderClass) {
return (TypeResolverBuilder<?>) findInstance(builderClass);
}
@Override
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
return (TypeIdResolver) findInstance(resolverClass);
}
private Object findInstance(Class<?> type) {
Object result = this.serializers.get(type);
return result != null ? result : BeanUtils.instantiateClass(type);
}
}
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2018-2019 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
*
* 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 org.springframework.hateoas.uber;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.uber.Jackson2UberModule.UberPagedResourcesDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
/**
* Jackson 2 mixin to handle {@link PagedResources} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberPagedResourcesDeserializer.class)
abstract class PagedResourcesMixin<T> extends PagedResources<T> {
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2018-2019 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
*
* 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 org.springframework.hateoas.uber;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourceDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
/**
* Jackson 2 mixin to handle {@link Resource} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberResourceDeserializer.class)
abstract class ResourceMixin<T> extends Resource<T> {
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2018-2019 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
*
* 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 org.springframework.hateoas.uber;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourceSupportDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
/**
* Jackson 2 mixin to handle {@link ResourceSupport} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberResourceSupportDeserializer.class)
abstract class ResourceSupportMixin extends ResourceSupport {
}

View File

@@ -1,32 +0,0 @@
/*
* Copyright 2018-2019 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
*
* 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 org.springframework.hateoas.uber;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.uber.Jackson2UberModule.UberResourcesDeserializer;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
/**
* Jackson 2 mixin to handle {@link Resources} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
*/
@JsonDeserialize(using = UberResourcesDeserializer.class)
abstract class ResourcesMixin<T> extends Resources<T> {
}

View File

@@ -20,9 +20,8 @@ import lombok.Value;
import lombok.experimental.Wither;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -47,7 +46,7 @@ class Uber {
@JsonCreator
Uber(@JsonProperty("version") String version, @JsonProperty("data") List<UberData> data,
@JsonProperty("error") UberError error) {
@JsonProperty("error") UberError error) {
this.version = version;
this.data = data;
@@ -64,11 +63,11 @@ class Uber {
* @return
*/
@JsonIgnore
List<Link> getLinks() {
Links getLinks() {
return this.data.stream()
.flatMap(uberData -> uberData.getLinks().stream())
.collect(Collectors.toList());
return data.stream() //
.flatMap(uberData -> uberData.getLinks().stream()) //
.collect(Links.collector());
}
}
}

View File

@@ -16,14 +16,13 @@
package org.springframework.hateoas.uber;
import static org.springframework.hateoas.uber.Jackson2UberModule.*;
import java.util.Arrays;
import org.springframework.hateoas.uber.Jackson2UberModule.UberActionDeserializer;
import org.springframework.http.HttpMethod;
import com.fasterxml.jackson.annotation.JsonValue;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
/**
* Embodies possible actions for an {@literal UBER+JSON} representation, mapped onto {@link HttpMethod}s.
@@ -32,7 +31,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
* @author Greg Turnquist
* @since 1.0
*/
@JsonSerialize(using = UberActionSerializer.class)
@JsonDeserialize(using = UberActionDeserializer.class)
enum UberAction {
@@ -55,7 +53,7 @@ enum UberAction {
* DELETE
*/
REMOVE(HttpMethod.DELETE),
/**
* PUT
*/
@@ -76,23 +74,24 @@ enum UberAction {
return this.httpMethod;
}
@JsonValue
@Override
public String toString() {
return this.name().toLowerCase();
}
/**
* Convert an {@link HttpMethod} into an {@link UberAction}.
*
* @param method
* @return
*/
static UberAction fromMethod(HttpMethod method) {
return Arrays.stream(UberAction.values())
.filter(action -> action.httpMethod == method)
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Unsupported method: " + method));
return Arrays.stream(UberAction.values()) //
.filter(action -> action.httpMethod == method) //
.findFirst() //
.orElseThrow(() -> new IllegalArgumentException("Unsupported method: " + method));
}
/**
@@ -104,4 +103,4 @@ enum UberAction {
static UberAction forRequestMethod(HttpMethod method) {
return HttpMethod.GET == method ? null : fromMethod(method);
}
}
}

View File

@@ -32,14 +32,19 @@ import org.springframework.http.MediaType;
* {@link AffordanceModelFactory} for {@literal UBER+JSON}.
*
* @author Greg Turnquist
* @since 1.0
* @author Oliver Drotbohm
*/
public class UberAffordanceModelFactory implements AffordanceModelFactory {
private final @Getter MediaType mediaType = MediaTypes.UBER_JSON;
/*
* (non-Javadoc)
* @see org.springframework.hateoas.AffordanceModelFactory#getAffordanceModel(java.lang.String, org.springframework.hateoas.Link, org.springframework.http.HttpMethod, org.springframework.core.ResolvableType, java.util.List, org.springframework.core.ResolvableType)
*/
@Override
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType, List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
public AffordanceModel getAffordanceModel(String name, Link link, HttpMethod httpMethod, ResolvableType inputType,
List<QueryParameter> queryMethodParameters, ResolvableType outputType) {
return new UberAffordanceModel(name, link, httpMethod, inputType, queryMethodParameters, outputType);
}
}

View File

@@ -15,8 +15,6 @@
*/
package org.springframework.hateoas.uber;
import static com.fasterxml.jackson.annotation.JsonInclude.*;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Value;
@@ -34,6 +32,8 @@ import java.util.stream.Collectors;
import org.springframework.hateoas.Affordance;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.Resource;
@@ -47,6 +47,7 @@ import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
@@ -64,7 +65,7 @@ class UberData {
private String id;
private String name;
private String label;
private List<String> rel;
private List<LinkRelation> rel;
private String url;
private UberAction action;
private boolean transclude;
@@ -75,12 +76,12 @@ class UberData {
private List<UberData> data;
@JsonCreator
UberData(@JsonProperty("id") String id, @JsonProperty("name") String name,
@JsonProperty("label") String label, @JsonProperty("rel") List<String> rel,
@JsonProperty("url") String url, @JsonProperty("action") UberAction action,
@JsonProperty("transclude") boolean transclude, @JsonProperty("model") String model,
@JsonProperty("sending") List<String> sending, @JsonProperty("accepting") List<String> accepting,
@JsonProperty("value") Object value, @JsonProperty("data") List<UberData> data) {
UberData(@JsonProperty("id") String id, @JsonProperty("name") String name, @JsonProperty("label") String label,
@JsonProperty("rel") List<LinkRelation> rel, @JsonProperty("url") String url,
@JsonProperty("action") UberAction action, @JsonProperty("transclude") boolean transclude,
@JsonProperty("model") String model, @JsonProperty("sending") List<String> sending,
@JsonProperty("accepting") List<String> accepting, @JsonProperty("value") Object value,
@JsonProperty("data") List<UberData> data) {
this.id = id;
this.name = name;
@@ -104,11 +105,7 @@ class UberData {
* Don't render if it's {@link UberAction#READ}.
*/
public UberAction getAction() {
if (this.action == UberAction.READ) {
return null;
}
return this.action;
return action == UberAction.READ ? null : action;
}
/*
@@ -116,9 +113,9 @@ class UberData {
*/
public Boolean isTemplated() {
return Optional.ofNullable(this.url)
.map(s -> s.contains("{?") ? true : null)
.orElse(null);
return Optional.ofNullable(this.url) //
.map(s -> s.contains("{?") ? true : null) //
.orElse(null);
}
/*
@@ -134,29 +131,23 @@ class UberData {
@JsonIgnore
public List<Link> getLinks() {
return Optional.ofNullable(this.rel)
.map(rels -> rels.stream()
.map(rel -> new Link(this.url, rel))
.collect(Collectors.toList()))
.orElse(Collections.emptyList());
return Optional.ofNullable(this.rel) //
.map(rels -> rels.stream() //
.map(rel -> new Link(this.url, rel)) //
.collect(Collectors.toList())) //
.orElse(Collections.emptyList());
}
/**
* Simple scalar types that can be encoded by value, not type.
*/
private final static HashSet<Class<?>> PRIMITIVE_TYPES = new HashSet<>(Arrays.asList(
String.class
));
private final static HashSet<Class<?>> PRIMITIVE_TYPES = new HashSet<>(Arrays.asList(String.class));
/**
* Set of all Spring HATEOAS resource types.
*/
private static final HashSet<Class<?>> RESOURCE_TYPES = new HashSet<>(Arrays.asList(
ResourceSupport.class,
Resource.class,
Resources.class,
PagedResources.class
));
private static final HashSet<Class<?>> RESOURCE_TYPES = new HashSet<>(
Arrays.asList(ResourceSupport.class, Resource.class, Resources.class, PagedResources.class));
/**
* Convert a {@link ResourceSupport} into a list of {@link UberData}s, containing links and content.
@@ -198,10 +189,8 @@ class UberData {
List<UberData> data = extractLinks(resources);
data.addAll(resources.getContent().stream()
.map(UberData::doExtractLinksAndContent)
.map(uberData -> new UberData().withData(uberData))
.collect(Collectors.toList()));
data.addAll(resources.getContent().stream().map(UberData::doExtractLinksAndContent)
.map(uberData -> new UberData().withData(uberData)).collect(Collectors.toList()));
return data;
}
@@ -210,23 +199,13 @@ class UberData {
List<UberData> collectionOfResources = extractLinksAndContent((Resources<?>) resources);
if (resources.getMetadata() != null ) {
if (resources.getMetadata() != null) {
collectionOfResources.add(new UberData()
.withName("page")
.withData(Arrays.asList(
new UberData()
.withName("number")
.withValue(resources.getMetadata().getNumber()),
new UberData()
.withName("size")
.withValue(resources.getMetadata().getSize()),
new UberData()
.withName("totalElements")
.withValue(resources.getMetadata().getTotalElements()),
new UberData()
.withName("totalPages")
.withValue(resources.getMetadata().getTotalPages()))));
collectionOfResources.add(new UberData().withName("page")
.withData(Arrays.asList(new UberData().withName("number").withValue(resources.getMetadata().getNumber()),
new UberData().withName("size").withValue(resources.getMetadata().getSize()),
new UberData().withName("totalElements").withValue(resources.getMetadata().getTotalElements()),
new UberData().withName("totalPages").withValue(resources.getMetadata().getTotalPages()))));
}
return collectionOfResources;
@@ -238,13 +217,13 @@ class UberData {
* @param links
* @return
*/
private static List<UberData> extractLinks(List<Link> links) {
private static List<UberData> extractLinks(Links links) {
return urlRelMap(links).entrySet().stream()
.map(entry -> new UberData()
.withUrl(entry.getKey())
.withRel(entry.getValue().getRels()))
.collect(Collectors.toList());
return urlRelMap(links).entrySet().stream() //
.map(entry -> new UberData() //
.withUrl(entry.getKey()) //
.withRel(entry.getValue().getRels())) //
.collect(Collectors.toList());
}
/**
@@ -277,14 +256,11 @@ class UberData {
*/
private static Optional<UberData> extractContent(Object content) {
if (!RESOURCE_TYPES.contains(content.getClass())) {
return Optional.of(new UberData()
.withName(StringUtils.uncapitalize(content.getClass().getSimpleName()))
.withData(extractProperties(content)));
}
return Optional.empty();
return Optional.of(content) //
.filter(it -> !RESOURCE_TYPES.contains(content.getClass())) //
.map(it -> new UberData() //
.withName(StringUtils.uncapitalize(it.getClass().getSimpleName())) //
.withData(extractProperties(it)));
}
/**
@@ -304,13 +280,12 @@ class UberData {
}
/**
* Turn a {@list List} of {@link Link}s into a {@link Map}, where you can see ALL the rels of a given
* link.
* Turn a {@list List} of {@link Link}s into a {@link Map}, where you can see ALL the rels of a given link.
*
* @param links
* @return a map with links mapping onto a {@link List} of rels
*/
private static Map<String, LinkAndRels> urlRelMap(List<Link> links) {
private static Map<String, LinkAndRels> urlRelMap(Links links) {
Map<String, LinkAndRels> urlRelMap = new LinkedHashMap<>();
@@ -329,43 +304,41 @@ class UberData {
* @param links
* @return
*/
private static List<UberData> extractAffordances(List<Link> links) {
private static List<UberData> extractAffordances(Links links) {
return links.stream()
.flatMap(link -> link.getAffordances().stream())
.map(affordance -> (UberAffordanceModel) affordance.getAffordanceModel(MediaTypes.UBER_JSON))
.map(model -> {
return links.stream() //
.flatMap(it -> it.getAffordances().stream()) //
.map(it -> it.getAffordanceModel(MediaTypes.UBER_JSON)) //
.map(UberAffordanceModel.class::cast) //
.map(it -> {
if (model.getHttpMethod().equals(HttpMethod.GET)) {
if (it.hasHttpMethod(HttpMethod.GET)) {
String suffix = model.getQueryProperties().stream()
.map(UberData::getName)
.collect(Collectors.joining(","));
String suffix = it.getQueryProperties().stream() //
.map(UberData::getName) //
.collect(Collectors.joining(","));
if (!model.getQueryMethodParameters().isEmpty()) {
suffix = "{?" + suffix + "}";
if (!it.getQueryMethodParameters().isEmpty()) {
suffix = "{?" + suffix + "}";
}
return new UberData() //
.withName(it.getName()) //
.withRel(Collections.singletonList(LinkRelation.of(it.getName())))
.withUrl(it.getLink().expand().getHref() + suffix) //
.withAction(it.getAction());
} else {
return new UberData() //
.withName(it.getName()) //
.withRel(Collections.singletonList(LinkRelation.of(it.getName())))
.withUrl(it.getLink().expand().getHref()).withModel(it.getInputProperties().stream() //
.map(UberData::getName).map(property -> property + "={" + property + "}") //
.collect(Collectors.joining("&")))
.withAction(it.getAction());
}
return new UberData()
.withName(model.getName())
.withRel(Collections.singletonList(model.getName()))
.withUrl(model.getLink().expand().getHref() + suffix)
.withAction(model.getAction());
} else {
return new UberData()
.withName(model.getName())
.withRel(Collections.singletonList(model.getName()))
.withUrl(model.getLink().expand().getHref())
.withModel(model.getInputProperties().stream()
.map(UberData::getName)
.map(property -> property + "={" + property + "}")
.collect(Collectors.joining("&")))
.withAction(model.getAction());
}
})
.collect(Collectors.toList());
}).collect(Collectors.toList());
}
/**
@@ -375,27 +348,26 @@ class UberData {
* @param links
* @return
*/
private static List<UberData> mergeDeclaredLinksIntoAffordanceLinks(List<UberData> affordanceBasedLinks, List<UberData> links) {
private static List<UberData> mergeDeclaredLinksIntoAffordanceLinks(List<UberData> affordanceBasedLinks,
List<UberData> links) {
return affordanceBasedLinks.stream()
.flatMap(affordance -> links.stream()
.filter(link -> link.getUrl().equals(affordance.getUrl()))
.map(link -> {
return affordanceBasedLinks.stream() //
.flatMap(affordance -> links.stream() //
.filter(link -> link.getUrl().equals(affordance.getUrl())) //
.map(link -> {
if (link.getAction() == affordance.getAction()) {
if (link.getAction() == affordance.getAction()) {
List<String> rels = new ArrayList<>(link.getRel());
List<LinkRelation> rels = new ArrayList<>(link.getRel());
rels.addAll(affordance.getRel());
rels.addAll(affordance.getRel());
return affordance
.withName(rels.get(0))
.withRel(rels);
} else {
return affordance;
}
}))
.collect(Collectors.toList());
return affordance.withName(rels.get(0).value()) //
.withRel(rels);
} else {
return affordance;
}
}))
.collect(Collectors.toList());
}
/**
@@ -407,26 +379,20 @@ class UberData {
private static List<UberData> extractProperties(Object obj) {
if (PRIMITIVE_TYPES.contains(obj.getClass())) {
return Collections.singletonList(new UberData()
.withValue(obj));
return Collections.singletonList(new UberData().withValue(obj));
}
return PropertyUtils.findProperties(obj).entrySet().stream()
.map(entry -> new UberData()
.withName(entry.getKey())
.withValue(entry.getValue()))
.collect(Collectors.toList());
.map(entry -> new UberData().withName(entry.getKey()).withValue(entry.getValue())).collect(Collectors.toList());
}
/**
* Holds both a {@link Link} and related {@literal rels}.
*
*/
@Data
private static class LinkAndRels {
private Link link;
private List<String> rels = new ArrayList<>();
private List<LinkRelation> rels = new ArrayList<>();
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2014-2018 the original author or authors.
* Copyright 2014-2019 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.
@@ -13,27 +13,28 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.hateoas.uber;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.stream.Collectors;
import java.util.Optional;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.LinkDiscoverer;
import org.springframework.hateoas.LinkRelation;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.MediaType;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Find links by rel in an {@literal UBER+JSON} representation.
*
* TODO: Pending https://github.com/json-path/JsonPath/issues/429, replace deserializing solution with JsonPath-based expression "$.uber.data[?(@.rel.indexOf('%s') != -1)].url"
* Find links by rel in an {@literal UBER+JSON} representation. TODO: Pending
* https://github.com/json-path/JsonPath/issues/429, replace deserializing solution with JsonPath-based expression
* "$.uber.data[?(@.rel.indexOf('%s') != -1)].url"
*
* @author Greg Turnquist
* @author Oliver Drotbohm
* @since 1.0
*/
public class UberLinkDiscoverer implements LinkDiscoverer {
@@ -46,40 +47,57 @@ public class UberLinkDiscoverer implements LinkDiscoverer {
this.mapper.registerModules(new Jackson2UberModule());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(org.springframework.hateoas.LinkRelation, java.lang.String)
*/
@Override
public Link findLinkWithRel(String rel, String representation) {
public Optional<Link> findLinkWithRel(LinkRelation rel, String representation) {
return getLinks(representation).stream()
.filter(link -> link.getRel().equals(rel))
.findFirst()
.orElse(null);
return getLinks(representation).stream() //
.filter(it -> it.hasRel(rel)) //
.findFirst();
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinkWithRel(org.springframework.hateoas.LinkRelation, java.io.InputStream)
*/
@Override
public Link findLinkWithRel(String rel, InputStream representation) {
public Optional<Link> findLinkWithRel(LinkRelation rel, InputStream representation) {
return getLinks(representation).stream()
.filter(link -> link.getRel().equals(rel))
.findFirst()
.orElse(null);
return getLinks(representation).stream() //
.filter(it -> it.hasRel(rel)) //
.findFirst();
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(org.springframework.hateoas.LinkRelation, java.lang.String)
*/
@Override
public List<Link> findLinksWithRel(String rel, String representation) {
public Links findLinksWithRel(LinkRelation rel, String representation) {
return getLinks(representation).stream()
.filter(link -> link.getRel().equals(rel))
.collect(Collectors.toList());
return getLinks(representation).stream().filter(it -> it.hasRel(rel)) //
.collect(Links.collector());
}
/*
* (non-Javadoc)
* @see org.springframework.hateoas.LinkDiscoverer#findLinksWithRel(org.springframework.hateoas.LinkRelation, java.io.InputStream)
*/
@Override
public List<Link> findLinksWithRel(String rel, InputStream representation) {
public Links findLinksWithRel(LinkRelation rel, InputStream representation) {
return getLinks(representation).stream()
.filter(link -> link.getRel().equals(rel))
.collect(Collectors.toList());
return getLinks(representation).stream() //
.filter(it -> it.hasRel(rel)) //
.collect(Links.collector());
}
/*
* (non-Javadoc)
* @see org.springframework.plugin.core.Plugin#supports(java.lang.Object)
*/
@Override
public boolean supports(MediaType delimiter) {
return delimiter.isCompatibleWith(MediaTypes.UBER_JSON);
@@ -91,7 +109,7 @@ public class UberLinkDiscoverer implements LinkDiscoverer {
* @param json
* @return
*/
private List<Link> getLinks(String json) {
private Links getLinks(String json) {
try {
return this.mapper.readValue(json, UberDocument.class).getUber().getLinks();
@@ -106,7 +124,7 @@ public class UberLinkDiscoverer implements LinkDiscoverer {
* @param stream
* @return
*/
private List<Link> getLinks(InputStream stream) {
private Links getLinks(InputStream stream) {
try {
return this.mapper.readValue(stream, UberDocument.class).getUber().getLinks();
@@ -114,4 +132,4 @@ public class UberLinkDiscoverer implements LinkDiscoverer {
throw new RuntimeException(e);
}
}
}
}