Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
  • Loading branch information
vivin committed Feb 7, 2015
1 parent 3ca6fa5 commit 9194808
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 33 deletions.
17 changes: 13 additions & 4 deletions src/main/java/org/springframework/hateoas/Link.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ public class Link implements Serializable {
@XmlAttribute private String rel;
@XmlAttribute private String href;
@XmlTransient @JsonIgnore private UriTemplate template;
@XmlTransient @JsonIgnore private Class<? extends ResourceSupport> owningResource;

/**
* Creates a new link to the given URI with the self rel.
Expand Down Expand Up @@ -194,10 +195,18 @@ private UriTemplate getUriTemplate() {
return template;
}

/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
public Class<? extends ResourceSupport> getOwningResource() {
return this.owningResource;
}

void setOwningResource(Class<? extends ResourceSupport> owningResource) {
this.owningResource = owningResource;
}

/*
* (non-Javadoc)
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public Link getId() {
*/
public void add(Link link) {
Assert.notNull(link, "Link must not be null!");
link.setOwningResource(this.getClass());
this.links.add(link);
}

Expand All @@ -64,6 +65,7 @@ public void add(Link link) {
public void add(Iterable<Link> links) {
Assert.notNull(links, "Given links must not be null!");
for (Link candidate : links) {
candidate.setOwningResource(this.getClass());
add(candidate);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2012 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 java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.hateoas.core.ControllerEntityLinks;

/**
* Annotation to force links under specified rels to be serialized as a JSON array regardless of cardinality.
*
* @author Vivin Paliath
*/
@Inherited
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ForceMultipleLinksOnRels {
public String[] value();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,11 @@

import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.*;

import org.springframework.beans.BeanUtils;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.hateoas.*;
import org.springframework.util.Assert;

import com.fasterxml.jackson.core.JsonGenerationException;
Expand Down Expand Up @@ -130,15 +120,29 @@ public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider)
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {


// keeps track of any rels that need their links to be forced as multiple (i.e., serialized to array)
// regardless of cardinality
List<String> forcedMultipleLinkRels = new ArrayList<String>();
if(!value.isEmpty()) {
Class<? extends ResourceSupport> owningResource = value.get(0).getOwningResource();
ForceMultipleLinksOnRels annotation = AnnotationUtils.findAnnotation(owningResource, ForceMultipleLinksOnRels.class);
if(annotation != null) {
forcedMultipleLinkRels.addAll(Arrays.asList(annotation.value()));
}
}

// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>();

List<Link> links = new ArrayList<Link>();

boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;

for (Link link : value) {


String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();

if (!link.getRel().equals(rel)) {
Expand Down Expand Up @@ -167,7 +171,7 @@ public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider p
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);

MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property), null);
provider.findKeySerializer(keyType, null), new OptionalListJackson2Serializer(property, forcedMultipleLinkRels), null);

serializer.serialize(sortedLinks, jgen, provider);
}
Expand Down Expand Up @@ -320,6 +324,7 @@ public static class OptionalListJackson2Serializer extends ContainerSerializer<O

private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers;
private final List<String> forcedMultipleLinkRels;

public OptionalListJackson2Serializer() {
this(null);
Expand All @@ -331,10 +336,24 @@ public OptionalListJackson2Serializer() {
* @param property
*/
public OptionalListJackson2Serializer(BeanProperty property) {
this(property, new ArrayList<String>());
}

/**
* Private constructor that creates a new instance using the given {@link BeanProperty}
* and a list of rels whose links need to be forced into an array representation regardless of
* cardinality.
*
* @param property
* @param forcedMultipleLinkRels
*/
private OptionalListJackson2Serializer(BeanProperty property, List<String> forcedMultipleLinkRels) {

super(List.class, false);

this.property = property;
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
this.forcedMultipleLinkRels = forcedMultipleLinkRels;
}

/*
Expand All @@ -360,14 +379,13 @@ public void serialize(Object value, JsonGenerator jgen, SerializerProvider provi
return;
}

if (list.size() == 1) {
serializeContents(list.iterator(), jgen, provider);
return;
if(list.size() > 1 || ((list.get(0) instanceof Link) && forcedMultipleLinkRels.contains(((Link) list.get(0)).getRel()))) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
} else {
serializeContents(list.iterator(), jgen, provider);
}

jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}

private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,8 @@

import org.junit.Before;
import org.junit.Test;
import org.springframework.hateoas.AbstractJackson2MarshallingIntegrationTest;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.PagedResources;
import org.springframework.hateoas.*;
import org.springframework.hateoas.PagedResources.PageMetadata;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.ResourceSupport;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.UriTemplate;
import org.springframework.hateoas.core.AnnotationRelProvider;
import org.springframework.hateoas.hal.Jackson2HalModule.HalHandlerInstantiator;

Expand All @@ -49,6 +42,7 @@
public class Jackson2HalIntegrationTest extends AbstractJackson2MarshallingIntegrationTest {

static final String SINGLE_LINK_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}}}";
static final String SINGLE_LINK_REFERENCE_AS_MULTIPLE = "{\"_links\":{\"multiple\":[{\"href\":\"localhost\"}]}}";
static final String LIST_LINK_REFERENCE = "{\"_links\":{\"self\":[{\"href\":\"localhost\"},{\"href\":\"localhost2\"}]}}";

static final String SIMPLE_EMBEDDED_RESOURCE_REFERENCE = "{\"_links\":{\"self\":{\"href\":\"localhost\"}},\"_embedded\":{\"content\":[\"first\",\"second\"]}}";
Expand Down Expand Up @@ -95,6 +89,13 @@ public void deserializeSingleLink() throws Exception {
assertThat(read(SINGLE_LINK_REFERENCE, ResourceSupport.class), is(expected));
}

@Test
public void rendersSingleLinkAsArrayWhenForced() throws Exception {
ResourceWithForcedMultipleLink expected = new ResourceWithForcedMultipleLink();
expected.add(new Link("localhost").withRel("multiple"));
assertThat(read(SINGLE_LINK_REFERENCE_AS_MULTIPLE, ResourceWithForcedMultipleLink.class), is(expected));
}

/**
* @see #29
*/
Expand Down Expand Up @@ -384,4 +385,8 @@ private static ObjectMapper getCuriedObjectMapper(CurieProvider provider) {

return mapper;
}

@ForceMultipleLinksOnRels({"multiple"})
private static class ResourceWithForcedMultipleLink extends ResourceSupport {
}
}

0 comments on commit 9194808

Please sign in to comment.