Skip to content

Commit c606bfd

Browse files
committed
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. Resolves #100,#417,#235,#240,#238,#223
1 parent 2f08735 commit c606bfd

File tree

8 files changed

+347
-9
lines changed

8 files changed

+347
-9
lines changed

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

Lines changed: 231 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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,34 @@ 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+
*/
115+
public Link(String href, String rel, String hreflang, String media, String title, String type, String deprecation) {
116+
117+
this(href, rel);
118+
this.hreflang = hreflang;
119+
this.media = media;
120+
this.title = title;
121+
this.type = type;
122+
this.deprecation = deprecation;
123+
}
124+
96125
/**
97126
* Empty constructor required by the marshalling framework.
98127
*/
@@ -118,6 +147,51 @@ public String getRel() {
118147
return rel;
119148
}
120149

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

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

213342
Link that = (Link) obj;
214343

215-
return this.href.equals(that.href) && this.rel.equals(that.rel);
344+
return
345+
this.href.equals(that.href)
346+
&&
347+
this.rel.equals(that.rel)
348+
&&
349+
(this.hreflang != null ? this.hreflang.equals(that.hreflang) : this.hreflang == that.hreflang)
350+
&&
351+
(this.media != null ? this.media.equals(that.media) : this.media == that.media)
352+
&&
353+
(this.title != null ? this.title.equals(that.title) : this.title == that.title)
354+
&&
355+
(this.type != null ? this.type.equals(that.type) : this.type == that.type)
356+
&&
357+
(this.deprecation != null ? this.deprecation.equals(that.deprecation) : this.deprecation == that.deprecation);
358+
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,30 @@ 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.toString() + "\"";
415+
}
416+
417+
418+
return linkString;
238419
}
239420

240421
/**
@@ -263,7 +444,33 @@ public static Link valueOf(String element) {
263444
throw new IllegalArgumentException("Link does not provide a rel attribute!");
264445
}
265446

266-
return new Link(matcher.group(1), attributes.get("rel"));
447+
if (!unrecognizedHeaders(attributes).isEmpty()) {
448+
throw new IllegalArgumentException("Link contains invalid RFC5988 headers!");
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,23 @@ 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+
addAll(attributes.keySet());
514+
}};
515+
516+
// Remove all recognized headers
517+
unrecognizedHeaders.removeAll(Arrays.asList("href", "rel", "hreflang", "media", "title", "type", "deprecation"));
518+
519+
return unrecognizedHeaders;
520+
}
295521
}

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

@@ -112,7 +113,7 @@ public static Links valueOf(String source) {
112113
if (!StringUtils.hasText(source)) {
113114
return NO_LINKS;
114115
}
115-
116+
116117
Matcher matcher = LINK_HEADER_PATTERN.matcher(source);
117118
List<Link> links = new ArrayList<Link>();
118119

src/main/java/org/springframework/hateoas/hal/LinkMixin.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,46 @@
2828
*
2929
* @author Alexander Baetz
3030
* @author Oliver Gierke
31+
* @author Greg Turnquist
3132
*/
32-
@JsonIgnoreProperties(value = "rel")
33+
@JsonIgnoreProperties({"rel", "media"})
3334
abstract class LinkMixin extends Link {
3435

3536
private static final long serialVersionUID = 4720588561299667409L;
3637

37-
/*
38+
/*
39+
* (non-Javadoc)
40+
* @see org.springframework.hateoas.Link#getHreflang()
41+
*/
42+
@Override
43+
@JsonInclude(Include.NON_NULL)
44+
public abstract String getHreflang();
45+
46+
/*
47+
* (non-Javadoc)
48+
* @see org.springframework.hateoas.Link#getTitle()
49+
*/
50+
@Override
51+
@JsonInclude(Include.NON_NULL)
52+
public abstract String getTitle();
53+
54+
/*
55+
* (non-Javadoc)
56+
* @see org.springframework.hateoas.Link#getType()
57+
*/
58+
@Override
59+
@JsonInclude(Include.NON_NULL)
60+
public abstract String getType();
61+
62+
/*
63+
* (non-Javadoc)
64+
* @see org.springframework.hateoas.Link#getDeprecation()
65+
*/
66+
@Override
67+
@JsonInclude(Include.NON_NULL)
68+
public abstract String getDeprecation();
69+
70+
/*
3871
* (non-Javadoc)
3972
* @see org.springframework.hateoas.Link#isTemplate()
4073
*/

0 commit comments

Comments
 (0)