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

Allow links on a particular rel to be displayed as an array even if there is only one link #288

Closed
vivin opened this issue Feb 6, 2015 · 23 comments
Assignees
Milestone

Comments

@vivin
Copy link

vivin commented Feb 6, 2015

Currently, Jackson2HalModule.HalLinkListSerializer will serialize a rel with only one link, into an object. If the rel has more than one link, it will serialize it into an array.

I know that rels have nothing to do with cardinality. However, for consistency's sake I would like to force certain a rel to always represent its links as being in an array. I ran into this issue in a case where I return a "collection" representation that contains links to individual resources. If I only have one element in the collection, the link is serialized as:

"ex:persons/person-resource": {
    "href": "..."
}

However, if I have multiple items, I will see:

"ex:persons/person-resource": [
    { "href": "..." },
    { "href": "..." }
]

From the perspective of an API user, this is remarkably inconsistent and confusing. I would rather that the representation not change based on the cardinality of elements. It is fine if the representation is always going to contain a link to a single element under some rel. In that case, it is alright that the link is serialized into a object (IMHO, it's a bit of a weakness because you don't see the same issue in HAL+XML where you can put a <link> element inside a <links> whether there is one or more <link>, but that's another issue). However in the case of a representation that is returning a collection of elements, I think it would be valuable from a consistency and usability perspective to always return the link as an array.

It would be nice if this was configurable when building the link somehow. A way, perhaps, to let the serializer know that the links should be serialized as an array instead of an object, regardless of cardinality.

Also, the HAL specification says:

Note: If you're unsure whether the link should be singular, assume it will be multiple. If you pick singular and find you need to change it, you will need to create a new link relation or face breaking existing clients.

Currently, there is no way to force a link to be multiple in Spring HATEOAS. I hope you guys will consider this feature request. I can also try and solve this problem myself and make a pull request.

If this is not feasible, or if I am misinterpreting the HAL specification, please let me know!

vivin added a commit to vivin/spring-hateoas that referenced this issue Feb 6, 2015
Allows links to be represented as a multiple even if there is only one.
vivin added a commit to vivin/spring-hateoas that referenced this issue Feb 7, 2015
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
vivin added a commit to vivin/spring-hateoas that referenced this issue Feb 7, 2015
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
vivin added a commit to vivin/spring-hateoas that referenced this issue Feb 7, 2015
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
vivin added a commit to vivin/spring-hateoas that referenced this issue Feb 7, 2015
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
vivin added a commit to vivin/spring-hateoas that referenced this issue Feb 13, 2015
This change allows links behind certain rels to always be wrapped by an array regardless of cardinality.
@schvanhugten
Copy link

I think the problem is in Jackson2HalModule:OptionalListJackson2Serializer:355 to 366 (version 0.16).

if (list.isEmpty()) {
    return;
}

if (list.size() == 1) {
    serializeContents(list.iterator(), jgen, provider);
    return;
}

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

You always want to write an array, so at least the if (list.size() == 1) { has to go. I did some work on a CustomHalLinkListSerializer that effectively does this. Code below for inspiration.

public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
        // sort links according to their relation
        Map<String, Object[]> sortedLinks = new LinkedHashMap<String, Object[]>();

        for (Link link : value) {

                String rel = link.getRel();
                Object[] existingLink = sortedLinks.get(rel);

                if (existingLink == null) {
                    sortedLinks.put(rel, new Object[] { link });
                } else {
                    // Resize array
                    sortedLinks.put(rel, ArrayUtils.add(existingLink, link));
                }
        }

        TypeFactory typeFactory = provider.getConfig().getTypeFactory();
        JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
        JavaType valueType = typeFactory.constructArrayType(Object.class);
        JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);

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

        serializer.serialize(sortedLinks, jgen, provider);
    }

@vivin
Copy link
Author

vivin commented Feb 19, 2015

Yup! I have a fix on this pull-request.

vivin added a commit to vivin/spring-hateoas that referenced this issue Mar 10, 2015
This change allows links behind certain rels to always be wrapped by an array regardless of cardinality.
@vivin
Copy link
Author

vivin commented Apr 16, 2015

Hello everyone,

I have a workaround for this issue, and I think you can use the same concepts for issue #324 as well. It's a little ugly and involves duplicating logic from Spring HATEOAS, but it works (at least for me). I'll post the code here, but when I get time, I'll set up a small demo project that showcases this workaround. I think this is a good stop-gap measure until my pull request is accepted or if Spring HATEOAS implements its own solution for this problem.

I got the idea after I read up on Jackson's MixIn Annotations. Using these, you can attach Jackson annotations to fields on classes, whose source you do not control.

The main problem with the current implementation is that the serializer for links always serializes a rel with a single link directly into a JSON object, and a rel with multiple links into an array of objects. There is no way to control the representation. With MixIn annotations, you can override the serializer for the links field in ResourceSupport and specify your own serializer. However, it's not as simple as that because there is some additional stuff you have to do, because the serializer requires access to some Spring Beans, and furthermore, Spring HATEOAS' custom module also controls the lifecycle of the serializers. So even if you set a MixIn annotation, the module does not know about it. I'll go through my workaround step by step. On a high level, the workaround involves creating some custom-serializers, maintaining the lifecycle of those serializers and their dependencies, and reconfiguring Spring HATEOAS' Jackson object-mapper to recognize our custom stuff:

Step 1: Create the mixin class:

public abstract class HalLinkListMixin {
    @JsonProperty("_links") @JsonSerialize(using = HalLinkListSerializer.class)
    public abstract List<Link> getLinks();
}

This mixin class will associate the HalLinkListSerializer serializer with the links property. This is a class that we will eventually create.

Step 2: Create a container class that holds the rels whose link representations should always be an array of link objects:

public class HalMultipleLinkRels {
    private final Set<String> rels;

    public HalMultipleLinkRels(String... rels) {
        this.rels = new HashSet<String>(Arrays.asList(rels));
    }

    public Set<String> getRels() {
        return Collections.unmodifiableSet(rels);
    }
}

Step 3: Create our new serializer that will override Spring HATEOAS's link-list serializer:

This class unfortunately duplicates logic, but it's not too bad. The key difference is that instead of using OptionalListJackson2Serializer, we use our own serializer (ListJackson2Serializer) that will force a rel's link representation as an array, if that rel exists in our container of rel overrides (HalMultipleLinkRels):

public class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {

    private final BeanProperty property;

    private CurieProvider curieProvider;

    private HalMultipleLinkRels halMultipleLinkRels;

    public HalLinkListSerializer() {
        this(null, null, new HalMultipleLinkRels());
    }

    public HalLinkListSerializer(CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
        this(null, curieProvider, halMultipleLinkRels);
    }

    public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
        super(List.class, false);
        this.property = property;
        this.curieProvider = curieProvider;
        this.halMultipleLinkRels = halMultipleLinkRels;
    }

    @Override
    public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {

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

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

        for (Link link : value) {

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

            if (!link.getRel().equals(rel)) {
                curiedLinkPresent = true;
            }

            if (sortedLinks.get(rel) == null) {
                sortedLinks.put(rel, new ArrayList<>());
            }

            links.add(link);
            sortedLinks.get(rel).add(link);
        }

        if (prefixingRequired && curiedLinkPresent) {

            ArrayList<Object> curies = new ArrayList<>();
            curies.add(curieProvider.getCurieInformation(new Links(links)));

            sortedLinks.put("curies", curies);
        }

        TypeFactory typeFactory = provider.getConfig().getTypeFactory();
        JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
        JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
        JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);

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

        serializer.serialize(sortedLinks, jgen, provider);
    }

    @Override
    public JavaType getContentType() {
        return null;
    }

    @Override
    public JsonSerializer<?> getContentSerializer() {
        return null;
    }

    @Override
    public boolean hasSingleElement(List<Link> value) {
        return value.size() == 1;
    }

    @Override
    protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
        return null;
    }

    @Override
    public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
        return new HalLinkListSerializer(property, curieProvider, halMultipleLinkRels);
    }

    private static class ListJackson2Serializer extends ContainerSerializer<Object> implements ContextualSerializer {

        private final BeanProperty property;
        private final Map<Class<?>, JsonSerializer<Object>> serializers = new HashMap<>();
        private final HalMultipleLinkRels halMultipleLinkRels;

        public ListJackson2Serializer() {
            this(null, null);
        }

        public ListJackson2Serializer(BeanProperty property, HalMultipleLinkRels halMultipleLinkRels) {
            super(List.class, false);

            this.property = property;
            this.halMultipleLinkRels = halMultipleLinkRels;
        }

        @Override
        public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {

            List<?> list = (List<?>) value;

            if (list.isEmpty()) {
                return;
            }

            if (list.size() == 1) {
                Object element = list.get(0);
                if (element instanceof Link) {
                    Link link = (Link) element;
                    if (halMultipleLinkRels.getRels().contains(link.getRel())) {
                        jgen.writeStartArray();
                        serializeContents(list.iterator(), jgen, provider);
                        jgen.writeEndArray();

                        return;
                    }
                }

                serializeContents(list.iterator(), jgen, provider);
                return;
            }

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

        @Override
        public JavaType getContentType() {
            return null;
        }

        @Override
        public JsonSerializer<?> getContentSerializer() {
            return null;
        }

        @Override
        public boolean hasSingleElement(Object value) {
            return false;
        }

        @Override
        protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
            throw new UnsupportedOperationException("not implemented");
        }

        @Override
        public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
            return new ListJackson2Serializer(property, halMultipleLinkRels);
        }

        private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {

            while (value.hasNext()) {
                Object elem = value.next();
                if (elem == null) {
                    provider.defaultSerializeNull(jgen);
                } else {
                    getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
                }
            }
        }

        private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider) throws JsonMappingException {

            JsonSerializer<Object> serializer = serializers.get(type);

            if (serializer == null) {
                serializer = provider.findValueSerializer(type, property);
                serializers.put(type, serializer);
            }

            return serializer;
        }
    }
}

Step 4: Deal with serializer lifecycle and dependency issues:

We have annotated the links property with @JsonSerializer(using = HalLinkListSerializer.class). Jackson will just new up the serializer that it finds in the annotation. This is fine if our serializer does not have any dependencies. But we do have dependencies on CurieProvider and HalMultipleLinkRels. These need to be initialized and provided to the serializer. You especially run into this issue when your serializer needs access to Spring beans. You cannot autowire them in, since Jackson news up the serializer, which bypasses Spring completely. The way to get around this is to create a class that implements HandlerInstantiator, which controls the lifecycle of the serializers. Spring HATEOAS does exactly this with Jackson2HalModule.HalHandlerInstantiator. The handler-instantiator creates instances of the serializers with the dependencies that they need, and maintains these serializer instances. So when Jackson sees the annotation, instead of creating a new instance, it requests the appropriate instance from the handler-instantiator. The problem is that Spring HATEOAS' handler-instantiator has no idea about our custom serializers. So how do we get around that? We have to create our own instantiator that maintains our custom serializers. This instantiator also maintains a private, internal instance of Jackson2HalModule.HalHandlerInstantiator and delegates to that, if Jackson requests a serializer-instance that our custom-instantiator does not know about. This way, I let Spring HATEOAS' instantiator handle the lifecycle and dependencies of the serializers that it knows about and also avoid duplicating logic:

public class HalHandlerInstantiator extends HandlerInstantiator {

    private final Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator;
    private final Map<Class<?>, JsonSerializer<?>> serializerMap = new HashMap<>();

    public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
        this(relProvider, curieProvider, halMultipleLinkRels, true);
    }

    public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels, boolean enforceEmbeddedCollections) {
        halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, enforceEmbeddedCollections);

        serializerMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, halMultipleLinkRels));
    }

    @Override
    public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
        return halHandlerInstantiator.deserializerInstance(config, annotated, deserClass);
    }

    @Override
    public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
        return halHandlerInstantiator.keyDeserializerInstance(config, annotated, keyDeserClass);
    }

    @Override
    public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
        if(serializerMap.containsKey(serClass)) {
            return serializerMap.get(serClass);
        } else {
            return halHandlerInstantiator.serializerInstance(config, annotated, serClass);
        }
    }

    @Override
    public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
        return halHandlerInstantiator.typeResolverBuilderInstance(config, annotated, builderClass);
    }

    @Override
    public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
        return halHandlerInstantiator.typeIdResolverInstance(config, annotated, resolverClass);
    }
}

Note: The neat thing about this instantiator is that if you have any custom serializers that require access to CurieProvider, RelProvider or any Spring Bean, you can throw them in here. You just have to make sure that you pass in the appropriate dependencies to the instantiator's constructor.

Step 5: Put it all together:

We have to reconfigure Spring HATEOAS' Jackson object-mapper to use our custom classes. You can do this in your application-configuration class. The code below is an example based on some actual code from a Spring Boot application that I maintain. I reconfigured the mapper inside a method that returns my HttpMessageConverters instance, since I'm already reconfiguring the Spring HATEOS' Jackson obkect-mapper to recognize application/*+json and application/json (I am using semantic media-types and content-negotation-based versioning). Notice that I'm getting all the dependencies I need and then passing them into the instantiator, which in turn provides those dependencies to the serializers that need them. This is also where I use the mix-in class to override the link list serializer for ResourceSupport. The code below has comments that explain what I am doing:

@Configuration
public class ApplicationConfiguration {

    private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
    private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";

    @Autowired
    private BeanFactory beanFactory;

    private static CurieProvider getCurieProvider(BeanFactory factory) {
        try {
            return factory.getBean(CurieProvider.class);
        } catch (NoSuchBeanDefinitionException e) {
            return null;
        }
    }

    ...

    @Bean
    public HttpMessageConverters customConverters() {
        CurieProvider curieProvider = getCurieProvider(beanFactory);
        RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
        ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);

        //Create a new instance of Spring HATEOAS' Jackson2HalModule
        SimpleModule module = new Jackson2HalModule();

        //Override the serializer for the link list in ResourceSupport using our mixin class
        module.setMixInAnnotation(ResourceSupport.class, HalLinkListMixin.class);

        //Register our module with the HAL object mapper
        halObjectMapper.registerModule(module);

        //Set the mapper's handler instantiator to our custom instantiator
        halObjectMapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, halMultipleLinkRels()));

        //The code below this line is just specific to my case.
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        converter.setSupportedMediaTypes(Arrays.asList(
                new MediaType("application", "json", Charset.defaultCharset()),
                new MediaType("application", "*+json", Charset.defaultCharset()),
                new MediaType("application", "hal+json")
        ));
        converter.setObjectMapper(halObjectMapper);

        return new HttpMessageConverters(converter);
    }

    //Populate our container class with the list of rels whose link representations must always be an
    //array of objects
    private HalMultipleLinkRels halMultipleLinkRels() {
        return new HalMultipleLinkRels(
            "order",
            "person",
            "blah"             
        );
    }
}

Note: For your case, you can probably just have a method annotated with @Bean that returns an instance of ObjectMapper. Everything would be exactly the same as the above, except for the message-converter code.

I hope this was helpful. This works for me, but I don't know if this is the best/most-elegant way of doing this, or if I am completely hosing Spring HATEOAS' regular behavior. Based on my tests, I haven't seen any strange behavior. Perhaps @olivergierke can comment if this is a valid workaround.

bjornblomqvist pushed a commit to bjornblomqvist/spring-hateoas that referenced this issue May 8, 2015
Allows links under a single rel to be represented as a multiple (i.e., serialized to an array) even if there is only one.
@odrotbohm
Copy link
Member

HalEmbeddedBuilder takes a boolean in the constructor to enable the preference of collection rels. Before starting to document what needs to be changed would you mind coming up with a test case that shows what you're doing, what that's rendered like, what you don't like about it and what you'd like to see instead? You could e.g. use that:

Person person = new Person("Dave", "Matthews");

EmbeddedWrappers wrappers = new EmbeddedWrappers(true);
EmbeddedWrapper embeddedPerson = wrappers.wrap(person);

System.out.println(mapper.writeValueAsString(new Resources<EmbeddedWrapper>(Arrays.asList(embeddedPerson))));

yields

{
  "_embedded" : {
    "persons" : [ {
      "firstname" : "Dave",
      "lastname" : "Matthews"
    } ]
  }
}

@vivin
Copy link
Author

vivin commented May 21, 2015

@olivergierke Is this addressed to me? I'm not using embedded resources; just regular links. Are you saying that this is applicable to links as well?

@vivin
Copy link
Author

vivin commented Sep 24, 2015

@olivergierke Something like that for the link builder would definitely be convenient, but that would mean that you would have to explicitly say that you want a collection every time you use that rel, and it would be easy to miss doing that in some place.

It would be better imo to enforce that across the entire API by doing that once, since a rel shouldn't have varying representations - it should either always be represented as a JSON object, or always be represented as a JSON array. With the first, the meaning is that the link rel always describes a single resource, and with the second the meaning is that the link rel describes multiple resources. The workaround I described above lets me do that right now and it works with embedded resources as well (need to verify this) since the custom serializer I have for links takes a look at the rel to figure out if the links/representations should be represented as a JSON object or a JSON array.

EDIT

I looked back at what I did and it looks like I just extended ResourceSupport and provided a method that serializes into the embedded representation based on whether that rel has been defined to be a collection-rel. I don't think I was aware of HalEmbeddedBuilder then. But it looks like HalEmbeddedBuilder can use the same technique I have in the workaround. Instead of using the boolean, it can check to see if that particular rel is a collection-rel and then return the embedded representation within a JSON array instead of a JSON object.

@vivin
Copy link
Author

vivin commented Sep 25, 2015

@olivergierke

What I would like to see is:

"ex:persons/person-resource": {
    "href": "..."
}

being represented as

"ex:persons/person-resource": [{
    "href": "..."
}]

If told to do so for that rel. For example, if the owning resource was PersonCollection, then something like the following would also work:

persons.add(linkTo(
    methodOn(PersonController.class).person(personId)
).withRel("person-resource").asLinkArray());

Or perhaps the constructor the the link builder can take a boolean (kind of like how you have for the embedded wrapper).

I have a patch where you can enforce this across the entire API. I concede that this may not be the right approach, and imo it's due to some ambiguity in the JSON+HAL standard. There's a sort of implicit cardinality conveyed via the representation when you have {} vs. []. But the representation should have no bearing on the meaning of the rel (including cardinality of links exposed by that rel), since it's the documentation of the rel that conveys that meaning. I think it's an artifact of the "feature" that JSON+HAL provides, where you can have {} for a single link and [] for multiple. The specification does say that if there will ever only be one link, then you should {}; otherwise you should use []. However I am not sure if that applies for every media-type that uses that particular rel, or if it is just for a particular media-type that is using that rel. If it is the latter, then you could see a particular rel being associated with {} in one representation, but [] in another, which makes things kind of inconsistent.

At any rate, I just need a way to make sure that a rel's links are always represented under [] regardless of the cardinality of links under it.

vivin added a commit to vivin/spring-hateoas that referenced this issue Sep 25, 2015
This change allows links behind certain rels to always be wrapped by an array regardless of cardinality.
@ledor473
Copy link

Any update on this issue?
I'd be interested to have that feature also, mainly because it makes parsing the JSON pretty hard without using spring-hateoas

@Berastau
Copy link

Is there any update on this? We are facing the same issue with our links

@ghost
Copy link

ghost commented Mar 2, 2017

Is there any update on this? I am using spring-boot-starter-hateos 1.5.1.RELEASE and am facing the same issues as people are describing above. A solution exactly like #288 (comment) would be ideal but for links and not embedded elements.

@ch4mpy
Copy link

ch4mpy commented Mar 22, 2017

Hello, I'd also like to force a _link rel to be an array even if it points to a single href.
In following sample, I expose need for both options.

  • self link should have a single rel (a Unique Resource Identifier)
  • instruments should be an array as a musician might play several instruments and it's way easier for clients to always expect an array of links to instruments resources than maybe an object and maybe an object array
{
  "firstName": "Canonball",
  "lastName": "Adderley",
  "_links": {
    "self": {
      "href": "http://localhost:8080/musicians/1"
    },
    "instruments": [
      {
        "href": "http://localhost:8080/referential/instruments/saxophone"
      }
    ]
  }
}

@vivin
Copy link
Author

vivin commented Mar 25, 2017

I think this project is largely abandoned. There doesn't seem to be much interest from the maintainer.

@gregturn
Copy link
Contributor

@vivin If the project isn't moving at your pace, what value do you find leaving such comments? FYI: I recently came back on the team and am developing a strategy to get it moving again. You'd know this if you checked the commit logs for the past week. Small stuff, but we're ramping back up.

@vivin
Copy link
Author

vivin commented Mar 25, 2017

@gregturn If you guys are seriously ramping back up, then that's great. But forgive me if I don't seem that enthusiastic. I've basically stopped using Spring HATEOAS, anyway. I was really excited when I first found out that Spring had a project for HATEOAS. When I ran into issues, I was eager to contribute to help resolve those issues as well. I opened this issue and others almost two years ago. I was quite engaged, and tried to have multiple discussions, proposed multiple alternatives, provided more than a few pull-requests, and even asked for clarity multiple times, without getting any sort of feedback from the maintainers -- I was basically ignored. This thread itself is a great example -- a question for clarification from the maintainer, multiple responses providing clarification by me, and others, and then nothing.

This is why I think that the project is abandoned.

But as I said before, if you guys really are ramping up, then that's great -- hopefully the project will be more engaged with those who are trying to contribute, this time.

@Berastau
Copy link

Does a corpse have pace?

@gregturn
Copy link
Contributor

@Berastau Sorry about the state of things in the past, but check the commit logs. There is motion. I'm warming up with doc changes, and other smaller stuff, while getting ramped up on the bigger ticket items. There's a bit of backlog, which I'm also trying to groom and capture some low hanging fruit.

@IainAdams
Copy link

@vivin @olivergierke - Spring has a Jackson HandlerInstantiator that uses the Application Context to source beans - thus allowing you to do Autowiring. Can't you use this instead? This is probably a separate improvement. If you agree, I will raise as such.

gregturn added a commit that referenced this issue May 22, 2017
Provide the means to render a single link entry as a JSON Array.

Original pull-request: #295
Related issues: #291
@gregturn gregturn self-assigned this May 22, 2017
gregturn added a commit that referenced this issue Sep 11, 2017
Provide the means to render a single link entry as a JSON Array.

Original pull-request: #295
Related issues: #291
odrotbohm pushed a commit that referenced this issue Oct 13, 2017
…ng options.

We now expose HalConfiguration to be defined as Spring bean in user configuration to control certain aspects of the HAL rendering. Initially we allow to control whether links shall always be rendered as collection. If no user-provided bean is available, we register a default instance.

Original pull request: #295
Related issues: #291
odrotbohm added a commit that referenced this issue Oct 13, 2017
Moved RenderSingleLinks enum into HalConfiguration. Simplified HalConfiguration setup by moving the default into the class. The lookup of a user-provided HalConfiguration is now handled on the bean name level to avoid premature initialization of a potentially defined bean. Formatting.

Original pull request: #295
Related issues: #291
@odrotbohm odrotbohm added this to the 0.24 milestone Oct 13, 2017
@luvz2code
Copy link

Is this issue still being looked at ? I see the overall status of the issue is closed.

@gregturn
Copy link
Contributor

gregturn commented Apr 3, 2018

See 7cd1fb8

@ghost
Copy link

ghost commented Nov 5, 2018

That commit appears to provide an option to make an all-or-none configuration change, but the questions in this thread are more geared toward picking and choosing which relationships should be represented as arrays. The unit test in that commit, for instance, represent self as an array which should never be the case.

@gregturn
Copy link
Contributor

@provDaveStimpert If we need to polish a unit test showing a different rel than self, that's fine. When it comes to rendering, the serializer doesn't know self from foobar.

The original use case presented reads:

Currently, Jackson2HalModule.HalLinkListSerializer will serialize a rel with only one link, into an object. If the rel has more than one link, it will serialize it into an array.

I know that rels have nothing to do with cardinality. However, for consistency's sake I would like to force certain a rel to always represent its links as being in an array. I ran into this issue in a case where I return a "collection" representation that contains links to individual resources.

At the time, it sounded like @vivin wanted to have single-item links turns into arrays, i.e. ALL link collections to be rendered as an array. Re-reading that today, with your emphasis, I can see the wrinkle of the possibility of ONLY doing this is the field is List<?>, and hence having a consistent representation.

But I feel we need a new ticket to capture it. We can link back to this one to track the conversion.

Honestly, fine tuning the representation of HAL documents is a good sign from a community perspective.

@vivin
Copy link
Author

vivin commented Dec 5, 2018

Just wanted to clarify why I originally opened the issue. It wasn't that I wanted all link collections to be rendered as an array; only links for certain rels:

I know that rels have nothing to do with cardinality. However, for [API] consistency's sake I would like to force a certain rel to always represent its links as being in an array.

So the idea was that it would be configurable. For example, if we had a resource Parent with a rel called children, then I wanted that to always be an array so that consumers of the API don't have to perform an additional check to see if it's an array or an object. I believe the HAL specification (at the time, anyway) mentioned that if a rel is defined to be associated with an array of links (in the API specification), then it should always be an array regardless of cardinality.

@odrotbohm
Copy link
Member

I guess need a new ticket for that then. One path we might want to explore is to connect the decision which way a relation is rendered with the already existing @Rel annotation, i.e. tweak the serializer in a way that it uses a flag declared in the annotation to overrule the global decision.

That said, I'd argue its good API design practice to decide for a globally consistent way of rendering rels as otherwise the client has to be made aware of the way to lookup the elements per rel which creates coupling.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants