Skip to content

Commit ab8eea2

Browse files
committed
#1038 - Update documentation and testing for ALPS.
1 parent 11485d4 commit ab8eea2

File tree

9 files changed

+416
-9
lines changed

9 files changed

+416
-9
lines changed

src/main/asciidoc/mediatypes.adoc

+81
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
= Media types
33
:code-dir: ../../../src/docs/java/org/springframework/hateoas
44
:resource-dir: ../../../src/docs/resources/org/springframework/hateoas
5+
:test-dir: ../../../src/test/java/org/springframework/hateoas
56
:linkattrs:
67

78
[[mediatypes.hal]]
@@ -408,6 +409,86 @@ https://github.com/spring-projects/spring-hateoas/issues[open a ticket, window="
408409

409410
NOTE: *UBER media type* is not associated in any way with *Uber Technologies Inc.*, the ride sharing company.
410411

412+
[[mediatypes.alps]]
413+
== ALPS - Application-Level Profile Semantics
414+
415+
https://tools.ietf.org/html/draft-amundsen-richardson-foster-alps-01[ALPS, window="_blank"] is a media type for providing
416+
profile-based metadata about another resource.
417+
418+
[quote, Mike Amundsen, ALPS spec]
419+
____
420+
An ALPS document can be used as a profile to
421+
explain the application semantics of a document with an application-
422+
agnostic media type (such as HTML, HAL, Collection+JSON, Siren,
423+
etc.). This increases the reusability of profile documents across
424+
media types.
425+
____
426+
427+
ALPS requires no special activation. Instead you "build" an `Alps` record and return it from either a Spring MVC or a Spring WebFlux web method as shown below:
428+
429+
.Building an `Alps` record
430+
====
431+
[source, java, tabsize=2, indent=0]
432+
----
433+
include::{test-dir}/support/WebMvcEmployeeController.java[tag=alps-profile]
434+
----
435+
* This example leverages `PropertyUtils.getExposedProperties()` to extract metadata about the domain object's attributes.
436+
====
437+
438+
This fragment has test data plugged in. It yields JSON like this:
439+
440+
.ALPS JSON
441+
====
442+
----
443+
{
444+
"version": "1.0",
445+
"doc": {
446+
"format": "TEXT",
447+
"href": "https://example.org/samples/full/doc.html",
448+
"value": "value goes here"
449+
},
450+
"descriptor": [
451+
{
452+
"id": "class field [name]",
453+
"name": "name",
454+
"type": "SEMANTIC",
455+
"descriptor": [
456+
{
457+
"id": "embedded"
458+
}
459+
],
460+
"ext": {
461+
"id": "ext [name]",
462+
"href": "https://example.org/samples/ext/name",
463+
"value": "value goes here"
464+
},
465+
"rt": "rt for [name]"
466+
},
467+
{
468+
"id": "class field [role]",
469+
"name": "role",
470+
"type": "SEMANTIC",
471+
"descriptor": [
472+
{
473+
"id": "embedded"
474+
}
475+
],
476+
"ext": {
477+
"id": "ext [role]",
478+
"href": "https://example.org/samples/ext/role",
479+
"value": "value goes here"
480+
},
481+
"rt": "rt for [role]"
482+
}
483+
]
484+
}
485+
----
486+
====
487+
488+
Instead of linking each field "automatically" to a domain object's fields, you can write them by hand if you like. It's also possible
489+
to use Spring Framework's message bundles and the `MessageSource` interface. This gives you the ability to delegate these values to
490+
locale-specific message bundles and even internationalize the metadata.
491+
411492
[[mediatypes.custom]]
412493
== Registering a custom media type
413494

src/main/java/org/springframework/hateoas/mediatype/alps/Alps.java

+13-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@
2424
import org.springframework.hateoas.mediatype.alps.Doc.DocBuilder;
2525
import org.springframework.hateoas.mediatype.alps.Ext.ExtBuilder;
2626

27+
import com.fasterxml.jackson.annotation.JsonCreator;
2728
import com.fasterxml.jackson.annotation.JsonInclude;
29+
import com.fasterxml.jackson.annotation.JsonProperty;
2830
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
2931

3032
/**
@@ -38,14 +40,23 @@
3840
*/
3941
@Value
4042
@Builder(builderMethodName = "alps")
41-
@JsonPropertyOrder({"version", "doc", "descriptor"})
43+
@JsonPropertyOrder({ "version", "doc", "descriptor" })
4244
@JsonInclude(JsonInclude.Include.NON_NULL)
4345
public class Alps {
4446

45-
private final String version = "1.0";
47+
private final String version;
4648
private final Doc doc;
4749
private final List<Descriptor> descriptor;
4850

51+
@JsonCreator
52+
private Alps(@JsonProperty("version") String version, @JsonProperty("doc") Doc doc,
53+
@JsonProperty("descriptor") List<Descriptor> descriptor) {
54+
55+
this.version = "1.0";
56+
this.doc = doc;
57+
this.descriptor = descriptor;
58+
}
59+
4960
/**
5061
* Returns a new {@link DescriptorBuilder}.
5162
*

src/main/java/org/springframework/hateoas/mediatype/alps/Descriptor.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@
2020

2121
import java.util.List;
2222

23+
import com.fasterxml.jackson.annotation.JsonCreator;
2324
import com.fasterxml.jackson.annotation.JsonInclude;
25+
import com.fasterxml.jackson.annotation.JsonProperty;
2426
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
2527

2628
/**
@@ -33,14 +35,32 @@
3335
*/
3436
@Value
3537
@Builder
36-
@JsonPropertyOrder({"id", "href", "name", "type", "doc", "descriptor", "ext"})
38+
@JsonPropertyOrder({ "id", "href", "name", "type", "doc", "descriptor", "ext" })
3739
@JsonInclude(JsonInclude.Include.NON_NULL)
3840
public class Descriptor {
3941

40-
private final String id, href, name;
42+
private final String id;
43+
private final String href;
44+
private final String name;
4145
private final Doc doc;
4246
private final Type type;
4347
private final Ext ext;
4448
private final String rt;
4549
private final List<Descriptor> descriptor;
50+
51+
@JsonCreator
52+
private Descriptor(@JsonProperty("id") String id, @JsonProperty("href") String href,
53+
@JsonProperty("name") String name, @JsonProperty("doc") Doc doc, @JsonProperty("type") Type type,
54+
@JsonProperty("ext") Ext ext, @JsonProperty("rt") String rt,
55+
@JsonProperty("descriptor") List<Descriptor> descriptor) {
56+
57+
this.id = id;
58+
this.href = href;
59+
this.name = name;
60+
this.doc = doc;
61+
this.type = type;
62+
this.ext = ext;
63+
this.rt = rt;
64+
this.descriptor = descriptor;
65+
}
4666
}

src/main/java/org/springframework/hateoas/mediatype/alps/Doc.java

+14-4
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@
1515
*/
1616
package org.springframework.hateoas.mediatype.alps;
1717

18-
import lombok.AllArgsConstructor;
1918
import lombok.Builder;
2019
import lombok.Value;
2120

2221
import org.springframework.util.Assert;
2322

23+
import com.fasterxml.jackson.annotation.JsonCreator;
2424
import com.fasterxml.jackson.annotation.JsonInclude;
25+
import com.fasterxml.jackson.annotation.JsonProperty;
2526
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
2627

2728
/**
@@ -34,12 +35,12 @@
3435
*/
3536
@Value
3637
@Builder
37-
@AllArgsConstructor
38-
@JsonPropertyOrder({"format", "href", "value"})
38+
@JsonPropertyOrder({ "format", "href", "value" })
3939
@JsonInclude(JsonInclude.Include.NON_NULL)
4040
public class Doc {
4141

42-
private final String href, value;
42+
private final String href;
43+
private final String value;
4344
private final Format format;
4445

4546
/**
@@ -57,4 +58,13 @@ public Doc(String value, Format format) {
5758
this.value = value;
5859
this.format = format;
5960
}
61+
62+
@JsonCreator
63+
private Doc(@JsonProperty("href") String href, @JsonProperty("value") String value,
64+
@JsonProperty("format") Format format) {
65+
66+
this.href = href;
67+
this.value = value;
68+
this.format = format;
69+
}
6070
}

src/main/java/org/springframework/hateoas/mediatype/alps/Ext.java

+11-1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
import lombok.Builder;
1919
import lombok.Value;
2020

21+
import com.fasterxml.jackson.annotation.JsonCreator;
22+
import com.fasterxml.jackson.annotation.JsonProperty;
2123
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
2224

2325
/**
@@ -30,10 +32,18 @@
3032
*/
3133
@Value
3234
@Builder
33-
@JsonPropertyOrder({"id", "href", "value"})
35+
@JsonPropertyOrder({ "id", "href", "value" })
3436
public class Ext {
3537

3638
private final String id;
3739
private final String href;
3840
private final String value;
41+
42+
@JsonCreator
43+
private Ext(@JsonProperty("id") String id, @JsonProperty("href") String href, @JsonProperty("value") String value) {
44+
45+
this.id = id;
46+
this.href = href;
47+
this.value = value;
48+
}
3949
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* Copyright 2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.hateoas.mediatype.alps;
17+
18+
import static org.hamcrest.Matchers.*;
19+
import static org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType.*;
20+
21+
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.extension.ExtendWith;
23+
import org.springframework.beans.factory.annotation.Autowired;
24+
import org.springframework.context.ApplicationContext;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.hateoas.MediaTypes;
28+
import org.springframework.hateoas.config.EnableHypermediaSupport;
29+
import org.springframework.hateoas.config.WebClientConfigurer;
30+
import org.springframework.hateoas.support.WebFluxEmployeeController;
31+
import org.springframework.test.context.ContextConfiguration;
32+
import org.springframework.test.context.junit.jupiter.SpringExtension;
33+
import org.springframework.test.web.reactive.server.WebTestClient;
34+
import org.springframework.web.reactive.config.EnableWebFlux;
35+
36+
/**
37+
* @author Greg Turnquist
38+
*/
39+
@ExtendWith(SpringExtension.class)
40+
@ContextConfiguration
41+
public class AlpsWebFluxIntegrationTest {
42+
43+
@Autowired WebTestClient testClient;
44+
45+
@Test
46+
void profileEndpointReturnsAlps() {
47+
48+
this.testClient.get().uri("/profile") //
49+
.accept(MediaTypes.ALPS_JSON) //
50+
.exchange() //
51+
.expectStatus().isOk() //
52+
.expectBody() //
53+
.jsonPath("$.version").isEqualTo("1.0") //
54+
.jsonPath("$.doc.format").isEqualTo("TEXT") //
55+
.jsonPath("$.doc.href").isEqualTo("https://example.org/samples/full/doc.html") //
56+
.jsonPath("$.doc.value").isEqualTo("value goes here") //
57+
.jsonPath("$.descriptor").value(hasSize(2)) //
58+
59+
.jsonPath("$.descriptor[0].id").isEqualTo("class field [name]") //
60+
.jsonPath("$.descriptor[0].name").isEqualTo("name") //
61+
.jsonPath("$.descriptor[0].type").isEqualTo("SEMANTIC") //
62+
.jsonPath("$.descriptor[0].descriptor").value(hasSize(1)) //
63+
.jsonPath("$.descriptor[0].descriptor[0].id").isEqualTo("embedded") //
64+
.jsonPath("$.descriptor[0].ext.id").isEqualTo("ext [name]") //
65+
.jsonPath("$.descriptor[0].ext.href").isEqualTo("https://example.org/samples/ext/name") //
66+
.jsonPath("$.descriptor[0].ext.value").isEqualTo("value goes here") //
67+
.jsonPath("$.descriptor[0].rt").isEqualTo("rt for [name]") //
68+
69+
.jsonPath("$.descriptor[1].id").isEqualTo("class field [role]") //
70+
.jsonPath("$.descriptor[1].name").isEqualTo("role") //
71+
.jsonPath("$.descriptor[1].type").isEqualTo("SEMANTIC") //
72+
.jsonPath("$.descriptor[1].descriptor").value(hasSize(1)) //
73+
.jsonPath("$.descriptor[1].descriptor[0].id").isEqualTo("embedded") //
74+
.jsonPath("$.descriptor[1].ext.id").isEqualTo("ext [role]") //
75+
.jsonPath("$.descriptor[1].ext.href").isEqualTo("https://example.org/samples/ext/role") //
76+
.jsonPath("$.descriptor[1].ext.value").isEqualTo("value goes here") //
77+
.jsonPath("$.descriptor[1].rt").isEqualTo("rt for [role]");
78+
}
79+
80+
@Configuration
81+
@EnableWebFlux
82+
@EnableHypermediaSupport(type = HAL)
83+
static class TestConfig {
84+
85+
@Bean
86+
WebFluxEmployeeController employeeController() {
87+
return new WebFluxEmployeeController();
88+
}
89+
90+
@Bean
91+
WebTestClient webTestClient(WebClientConfigurer webClientConfigurer, ApplicationContext ctx) {
92+
93+
return WebTestClient.bindToApplicationContext(ctx).build() //
94+
.mutate() //
95+
.exchangeStrategies(webClientConfigurer.hypermediaExchangeStrategies()) //
96+
.build();
97+
}
98+
}
99+
100+
}

0 commit comments

Comments
 (0)