Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify the Link class to support RFC 5988 Target Attributes #238

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 180 additions & 6 deletions src/main/java/org/springframework/hateoas/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@
package org.springframework.hateoas;

import java.io.Serializable;
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 java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;

import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonInclude;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

Expand Down Expand Up @@ -55,6 +60,7 @@ public class Link implements Serializable {
@XmlAttribute private String rel;
@XmlAttribute private String href;
@XmlTransient @JsonIgnore private UriTemplate template;
private Map<String,Object> attributes = new TreeMap<String, Object>();

/**
* Creates a new link to the given URI with the self rel.
Expand Down Expand Up @@ -99,6 +105,17 @@ protected Link() {

}

/**
* Copy constructor needed for the various with methods.
*/
private Link(Link linkToCopy) {

this.template = linkToCopy.template;
this.href = linkToCopy.href;
this.rel = linkToCopy.rel;
this.attributes = linkToCopy.attributes;
}

/**
* Returns the actual URI the link is pointing to.
*
Expand All @@ -117,6 +134,18 @@ public String getRel() {
return rel;
}

/**
* Returns the attributes of the link.
*
* @return
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
@JsonAnyGetter
public Map<String,Object> getAttributes() {

return Collections.unmodifiableMap(attributes);
}

/**
* Returns a {@link Link} pointing to the same URI but with the given relation.
*
Expand All @@ -136,6 +165,83 @@ public Link withSelfRel() {
return withRel(Link.REL_SELF);
}

/**
* Returns a {@link Link} pointing to the same URI but with the specified anchor value.
*
* @param anchor
* @return
*/
public Link withAnchor(String anchor) {

Link link = new Link(this);
link.setAttributeValue("anchor", anchor);
return link;
}

/**
* Returns a {@link Link} pointing to the same URI but with the specified hreflang value.
*
* @param hreflang
* @return
*/
public Link withHreflang(String hreflang) {

return withAttribute("hreflang", hreflang);
}

/**
* Returns a {@link Link} pointing to the same URI but with the specified media value.
*
* @param media
* @return
*/
public Link withMedia(String media) {

Link link = new Link(this);
link.setAttributeValue("media", media);
return link;
}

/**
* Returns a {@link Link} pointing to the same URI but with the specified title value.
*
* @param title
* @return
*/
public Link withTitle(String title) {

Link link = new Link(this);
link.setAttributeValue("title", title);
return link;
}

/**
* Returns a {@link Link} pointing to the same URI but with the specified type value.
*
* @param type
* @return
*/
public Link withType(String type) {

Link link = new Link(this);
link.setAttributeValue("type", type);
return link;
}

/**
* Returns a {@link Link} pointing to the same URI but with the specified attribute
*
* @param key
* @param value
* @return
*/
public Link withAttribute(String key, String value) {

Link link = new Link(this);
link.setAttributeValue(key, value);
return link;
}

/**
* Returns the variable names contained in the template.
*
Expand Down Expand Up @@ -233,7 +339,59 @@ public int hashCode() {
*/
@Override
public String toString() {
return String.format("<%s>;rel=\"%s\"", href, rel);

StringBuilder str = new StringBuilder();

str.append("<");
str.append(href);
str.append(">");

if (rel != null) {
str.append(";rel=\"");
str.append(rel);
str.append("\"");
}

for (String key : attributes.keySet()) {
Object value = attributes.get(key);

if (value instanceof Collection) {
for (String item : (Collection<String>)value) {
str.append(";");
str.append(key);
str.append("=\"");
str.append(item);
str.append("\"");
}
}
else {
str.append(";");
str.append(key);
str.append("=\"");
str.append(value);
str.append("\"");
}
}

return str.toString();
}

private void setAttributeValue(String key, String value) {

Object currentValue = attributes.get(key);

if (currentValue instanceof Collection) {
((Collection<String>)currentValue).add(value);
}
else if (currentValue instanceof String) {
Collection<String> values = new ArrayList<String>();
attributes.put(key, values);
values.add(currentValue.toString());
values.add(value);
}
else {
attributes.put(key, value);
}
}

/**
Expand Down Expand Up @@ -262,7 +420,15 @@ public static Link valueOf(String element) {
throw new IllegalArgumentException("Link does not provide a rel attribute!");
}

return new Link(matcher.group(1), attributes.get("rel"));
Link link = new Link(matcher.group(1), attributes.get("rel"));

for (String key : attributes.keySet()) {
if (!key.equalsIgnoreCase("rel")) {
link = link.withAttribute(key, attributes.get(key));
}
}

return link;

} else {
throw new IllegalArgumentException(String.format("Given link header %s is not RFC5988 compliant!", element));
Expand All @@ -282,11 +448,19 @@ private static Map<String, String> getAttributeMap(String source) {
}

Map<String, String> attributes = new HashMap<String, String>();
Pattern keyAndValue = Pattern.compile("(\\w+)=\\\"(\\p{Alnum}*)\"");
Matcher matcher = keyAndValue.matcher(source);

while (matcher.find()) {
attributes.put(matcher.group(1), matcher.group(2));
Pattern attributesPattern = Pattern.compile("\\w+=\\\"[\\s\\p{Alnum}]*\"*");
Pattern keyAndValuePattern = Pattern.compile("(\\w+)=\\\"([\\s\\p{Alnum}]*)\"");

Matcher attributesMatcher = attributesPattern.matcher(source);

while (attributesMatcher.find()) {
String group = attributesMatcher.group();
Matcher keyValueMatcher = keyAndValuePattern.matcher(group);

if (keyValueMatcher.find()) {
attributes.put(keyValueMatcher.group(1), keyValueMatcher.group(2));
}
}

return attributes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ public interface CurieProvider {
*/
String 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
*/
String getNamespacedRelFrom(String rel);

/**
* Returns an object to render as the base curie information. Implementations have to make sure, the retunred
* instances renders as defined in the spec.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,15 @@ public Collection<? extends Object> getCurieInformation(Links links) {
@Override
public String getNamespacedRelFrom(Link link) {

String rel = link.getRel();
return getNamespacedRelFrom(link.getRel());
}

/*
* (non-Javadoc)
* @see org.springframework.hateoas.hal.CurieProvider#getNamespacedRelFrom(java.lang.String)
*/
@Override
public String getNamespacedRelFrom(String rel) {

boolean prefixingNeeded = !IanaRels.isIanaRel(rel) && !rel.contains(":");
return prefixingNeeded ? String.format("%s:%s", curie.name, rel) : rel;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.List;
import java.util.Map;

import org.springframework.hateoas.Link;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.core.EmbeddedWrapper;
Expand All @@ -32,7 +33,7 @@
/**
* 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
*/
Expand All @@ -42,27 +43,29 @@ class HalEmbeddedBuilder {

private final Map<String, Object> embeddeds = new HashMap<String, Object>();
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.
*/
public HalEmbeddedBuilder(RelProvider provider, boolean preferCollectionRels) {
public HalEmbeddedBuilder(RelProvider provider, CurieProvider curieProvider, boolean preferCollectionRels) {

Assert.notNull(provider, "Relprovider must not be null!");

this.provider = provider;
this.curieProvider = curieProvider;
this.wrappers = new EmbeddedWrappers(preferCollectionRels);
}

/**
* 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 value can be {@literal null}.
*
* @param source can be {@literal null}.
*/
public void add(Object source) {

Expand Down Expand Up @@ -116,12 +119,17 @@ private String getDefaultedRelFor(EmbeddedWrapper wrapper, boolean forCollection
Class<?> type = wrapper.getRelTargetType();

String rel = forCollection ? provider.getCollectionResourceRelFor(type) : provider.getItemResourceRelFor(type);

if (curieProvider != null) {
rel = curieProvider.getNamespacedRelFrom(rel);
}

return rel == null ? DEFAULT_REL : rel;
}

/**
* Returns the added objects keyed up by their relation types.
*
*
* @return
*/
public Map<String, Object> asMap() {
Expand Down
Loading