Skip to content

Commit 2dfa996

Browse files
committed
#567 - Add extra attributes to Link and use for HAL mediatype.
Adds many additional attributes defined in RFC5988 and verifies they work properly in the neutral representation of Link while also being rendered properly in the HAL module. Related tickets: #100, #417, #235, #240, #238, #223
1 parent aafe1f2 commit 2dfa996

File tree

8 files changed

+379
-16
lines changed

8 files changed

+379
-16
lines changed

src/main/java/org/springframework/hateoas/Link.java

+231-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2016 the original author or authors.
2+
* Copyright 2012-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,10 +16,13 @@
1616
package org.springframework.hateoas;
1717

1818
import java.io.Serializable;
19+
import java.util.Arrays;
1920
import java.util.Collections;
2021
import java.util.HashMap;
22+
import java.util.HashSet;
2123
import java.util.List;
2224
import java.util.Map;
25+
import java.util.Set;
2326
import java.util.regex.Matcher;
2427
import java.util.regex.Pattern;
2528

@@ -37,6 +40,7 @@
3740
* Value object for links.
3841
*
3942
* @author Oliver Gierke
43+
* @author Greg Turnquist
4044
*/
4145
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
4246
@JsonIgnoreProperties("templated")
@@ -55,6 +59,11 @@ public class Link implements Serializable {
5559

5660
@XmlAttribute private String rel;
5761
@XmlAttribute private String href;
62+
@XmlAttribute private String hreflang;
63+
@XmlAttribute private String media;
64+
@XmlAttribute private String title;
65+
@XmlAttribute private String type;
66+
@XmlAttribute private String deprecation;
5867
@XmlTransient @JsonIgnore private UriTemplate template;
5968

6069
/**
@@ -85,14 +94,35 @@ public Link(String href, String rel) {
8594
*/
8695
public Link(UriTemplate template, String rel) {
8796

88-
Assert.notNull(template, "UriTempalte must not be null!");
97+
Assert.notNull(template, "UriTemplate must not be null!");
8998
Assert.hasText(rel, "Rel must not be null or empty!");
9099

91100
this.template = template;
92101
this.href = template.toString();
93102
this.rel = rel;
94103
}
95104

105+
/**
106+
* Creates a new {@link Link} to the given URI with the given rel, hreflang, media, title, and type.
107+
*
108+
* @param href must not be {@literal null} or empty.
109+
* @param rel must not be {@literal null} or empty.
110+
* @param hreflang
111+
* @param media
112+
* @param title
113+
* @param type
114+
* @param deprecation
115+
*/
116+
public Link(String href, String rel, String hreflang, String media, String title, String type, String deprecation) {
117+
118+
this(href, rel);
119+
this.hreflang = hreflang;
120+
this.media = media;
121+
this.title = title;
122+
this.type = type;
123+
this.deprecation = deprecation;
124+
}
125+
96126
/**
97127
* Empty constructor required by the marshalling framework.
98128
*/
@@ -118,6 +148,51 @@ public String getRel() {
118148
return rel;
119149
}
120150

151+
/**
152+
* Returns the hreflang of the link.
153+
*
154+
* @return
155+
*/
156+
public String getHreflang() {
157+
return hreflang;
158+
}
159+
160+
/**
161+
* Returns the media of the link.
162+
*
163+
* @return
164+
*/
165+
public String getMedia() {
166+
return media;
167+
}
168+
169+
/**
170+
* Returns the title of the link.
171+
*
172+
* @return
173+
*/
174+
public String getTitle() {
175+
return title;
176+
}
177+
178+
/**
179+
* Returns the type of the link
180+
*
181+
* @return
182+
*/
183+
public String getType() {
184+
return type;
185+
}
186+
187+
/**
188+
* Returns link about deprecation of this link.
189+
*
190+
* @return
191+
*/
192+
public String getDeprecation() {
193+
return deprecation;
194+
}
195+
121196
/**
122197
* Returns a {@link Link} pointing to the same URI but with the given relation.
123198
*
@@ -137,6 +212,61 @@ public Link withSelfRel() {
137212
return withRel(Link.REL_SELF);
138213
}
139214

215+
/**
216+
* Returns a {@link Link} with the {@code hreflang} attribute filled out.
217+
*
218+
* @param hreflang
219+
* @return
220+
*/
221+
public Link withHreflang(String hreflang) {
222+
Assert.hasText(hreflang, "hreflang must not be null or empty!");
223+
return new Link(this.href, this.rel, hreflang, this.media, this.title, this.type, this.deprecation);
224+
}
225+
226+
/**
227+
* Returns a {@link Link} with the {@code media} attribute filled out.
228+
*
229+
* @param media
230+
* @return
231+
*/
232+
public Link withMedia(String media) {
233+
Assert.hasText(media, "media must not be null or empty!");
234+
return new Link(this.href, this.rel, this.hreflang, media, this.title, this.type, this.deprecation);
235+
}
236+
237+
/**
238+
* Returns a {@link Link} with the {@code title} attribute filled out.
239+
*
240+
* @param title
241+
* @return
242+
*/
243+
public Link withTitle(String title) {
244+
Assert.hasText(title, "title must not be null or empty!");
245+
return new Link(this.href, this.rel, this.hreflang, this.media, title, this.type, this.deprecation);
246+
}
247+
248+
/**
249+
* Returns a {@link Link} with the {@code type} attribute filled out.
250+
*
251+
* @param type
252+
* @return
253+
*/
254+
public Link withType(String type) {
255+
Assert.hasText(type, "type must not be null or empty!");
256+
return new Link(this.href, this.rel, this.hreflang, this.media, this.title, type, this.deprecation);
257+
}
258+
259+
/**
260+
* Returns a {@link Link} with the {@code deprecation} attribute filled out.
261+
*
262+
* @param deprecation
263+
* @return
264+
*/
265+
public Link withDeprecation(String deprecation) {
266+
Assert.hasText(deprecation, "deprecation must not be null or empty!");
267+
return new Link(this.href, this.rel, this.hreflang, this.media, this.title, this.type, deprecation);
268+
}
269+
140270
/**
141271
* Returns the variable names contained in the template.
142272
*
@@ -212,7 +342,20 @@ public boolean equals(Object obj) {
212342

213343
Link that = (Link) obj;
214344

215-
return this.href.equals(that.href) && this.rel.equals(that.rel);
345+
return
346+
this.href.equals(that.href)
347+
&&
348+
this.rel.equals(that.rel)
349+
&&
350+
(this.hreflang != null ? this.hreflang.equals(that.hreflang) : this.hreflang == that.hreflang)
351+
&&
352+
(this.media != null ? this.media.equals(that.media) : this.media == that.media)
353+
&&
354+
(this.title != null ? this.title.equals(that.title) : this.title == that.title)
355+
&&
356+
(this.type != null ? this.type.equals(that.type) : this.type == that.type)
357+
&&
358+
(this.deprecation != null ? this.deprecation.equals(that.deprecation) : this.deprecation == that.deprecation);
216359
}
217360

218361
/*
@@ -225,6 +368,21 @@ public int hashCode() {
225368
int result = 17;
226369
result += 31 * href.hashCode();
227370
result += 31 * rel.hashCode();
371+
if (hreflang != null) {
372+
result += 31 * hreflang.hashCode();
373+
}
374+
if (media != null) {
375+
result += 31 * media.hashCode();
376+
}
377+
if (title != null) {
378+
result += 31 * title.hashCode();
379+
}
380+
if (type != null) {
381+
result += 31 * type.hashCode();
382+
}
383+
if (deprecation != null) {
384+
result += 31 * deprecation.hashCode();
385+
}
228386
return result;
229387
}
230388

@@ -234,7 +392,29 @@ public int hashCode() {
234392
*/
235393
@Override
236394
public String toString() {
237-
return String.format("<%s>;rel=\"%s\"", href, rel);
395+
String linkString = String.format("<%s>;rel=\"%s\"", href, rel);
396+
397+
if (hreflang != null) {
398+
linkString += ";hreflang=\"" + hreflang + "\"";
399+
}
400+
401+
if (media != null) {
402+
linkString += ";media=\"" + media + "\"";
403+
}
404+
405+
if (title != null) {
406+
linkString += ";title=\"" + title + "\"";
407+
}
408+
409+
if (type != null) {
410+
linkString += ";type=\"" + type + "\"";
411+
}
412+
413+
if (deprecation != null) {
414+
linkString += ";deprecation=\"" + deprecation + "\"";
415+
}
416+
417+
return linkString;
238418
}
239419

240420
/**
@@ -263,7 +443,34 @@ public static Link valueOf(String element) {
263443
throw new IllegalArgumentException("Link does not provide a rel attribute!");
264444
}
265445

266-
return new Link(matcher.group(1), attributes.get("rel"));
446+
Set<String> unrecognizedHeaders = unrecognizedHeaders(attributes);
447+
if (!unrecognizedHeaders.isEmpty()) {
448+
throw new IllegalArgumentException("Link contains invalid RFC5988 headers! => " + unrecognizedHeaders);
449+
}
450+
451+
Link link = new Link(matcher.group(1), attributes.get("rel"));
452+
453+
if (attributes.containsKey("hreflang")) {
454+
link = link.withHreflang(attributes.get("hreflang"));
455+
}
456+
457+
if (attributes.containsKey("media")) {
458+
link = link.withMedia(attributes.get("media"));
459+
}
460+
461+
if (attributes.containsKey("title")) {
462+
link = link.withTitle(attributes.get("title"));
463+
}
464+
465+
if (attributes.containsKey("type")) {
466+
link = link.withType(attributes.get("type"));
467+
}
468+
469+
if (attributes.containsKey("deprecation")) {
470+
link = link.withDeprecation(attributes.get("deprecation"));
471+
}
472+
473+
return link;
267474

268475
} else {
269476
throw new IllegalArgumentException(String.format("Given link header %s is not RFC5988 compliant!", element));
@@ -283,7 +490,7 @@ private static Map<String, String> getAttributeMap(String source) {
283490
}
284491

285492
Map<String, String> attributes = new HashMap<String, String>();
286-
Pattern keyAndValue = Pattern.compile("(\\w+)=\"(\\p{Lower}[\\p{Lower}\\p{Digit}\\.\\-]*|" + URI_PATTERN + ")\"");
493+
Pattern keyAndValue = Pattern.compile("(\\w+)=\"(\\p{Lower}[\\p{Lower}\\p{Digit}\\.\\-\\s]*|" + URI_PATTERN + ")\"");
287494
Matcher matcher = keyAndValue.matcher(source);
288495

289496
while (matcher.find()) {
@@ -292,4 +499,22 @@ private static Map<String, String> getAttributeMap(String source) {
292499

293500
return attributes;
294501
}
502+
503+
/**
504+
* Scan for any headers not recognized.
505+
*
506+
* @param attributes
507+
* @return
508+
*/
509+
private static Set<String> unrecognizedHeaders(final Map<String, String> attributes) {
510+
511+
// Copy the existing keys to avoid damaging the original.
512+
Set<String> unrecognizedHeaders = new HashSet<String>();
513+
unrecognizedHeaders.addAll(attributes.keySet());
514+
515+
// Remove all recognized headers
516+
unrecognizedHeaders.removeAll(Arrays.asList("href", "rel", "hreflang", "media", "title", "type", "deprecation"));
517+
518+
return unrecognizedHeaders;
519+
}
295520
}

src/main/java/org/springframework/hateoas/Links.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2013-2016 the original author or authors.
2+
* Copyright 2013-2017 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,10 +29,11 @@
2929
* Value object to represent a list of {@link Link}s.
3030
*
3131
* @author Oliver Gierke
32+
* @author Greg Turnquist
3233
*/
3334
public class Links implements Iterable<Link> {
3435

35-
private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("(<[^>]*>;rel=\"[^\"]*\")");
36+
private static final Pattern LINK_HEADER_PATTERN = Pattern.compile("(<[^>]*>(;\\w+=\"[^\"]*\")+)");
3637

3738
static final Links NO_LINKS = new Links(Collections.<Link> emptyList());
3839

0 commit comments

Comments
 (0)