Coder Social home page Coder Social logo

hydra-java's Introduction

hydra-java Build Status

Problem

The meaning of json attributes in api responses, their possible values etc. is usually not obvious without referring to some information coming from outside the resource itself. That is due to the nature of json. Two solutions immediately come to mind. Both are ways of vendor-specific documentation, some are machine-readable, some aren’t.

Describe the type in some sort of json-schema, wadl, raml, swagger or similar and publish it together with the resource. People could even generate classes from this information, if they wish to. My api users coming from a wsdl background scream for something like that.

Or put up documentation pages to describe your ex:doodad extension relation types and make the documentation available by dereferencing http://example.com/api/rels#doodad.

But one of the rules for a ReSTful API is:

A REST API should never have “typed” resources that are significant to the client. Specification authors may use resource types for describing server implementation behind the interface, but those types must be irrelevant and invisible to the client. The only types that are significant to a client are the current representation’s media type and standardized relation names. [Failure here implies that clients are assuming a resource structure due to out-of band information, such as a domain-specific standard, which is the data-oriented equivalent to RPC’s functional coupling].

— Roy Fielding

My interpretation of this famous rant by Roy Fielding:

A publicly available media-type should give clients all necessary means to interpret a server response, and relation names for hyperlinks in the response must be recognizable based on public conventions, so that the client can act upon the responses it receives without knowing the details of a vendor-specific api.

In other words: If a client is told to make a reservation for a concert ticket, it should be able to recognize what one-fancy-api requires to achieve that without processing a vendor-specific documentation. How can we do that, purely based on a media type and relation names? Do we need hundreds of iana registered media types for all kinds of purposes?

Solution (evolving)

I see json-ld (media type application/ld+json) as a possible way to solve this problem without forcing people to ask me about my vendor-specific documentation, thus decoupling the clients from my server types.

Clients should be able to understand a response based on widely available, standardized, public information.

The json-ld mediatype allows to bring descriptions of things in the real world from public vocabularies into your json files. With json-ld there is a way to say that a json response describes a MusicEvent which offers a Ticket without any vendor-specific documentation, and it can also link to other resources.

A popular vocabulary which describes things on the internet is http://schema.org. It is used by all major search engines for search engine optimization and sufficient for basic needs. It also integrates with other vocabularies, e.g. by using additionalType to point to GoodRelations classes or by using external enumerated values as shown by DeliveryMethod.

(For those of you about to say that the Semantic Web never took off, please note that json-ld is not about the Semantic Web at all).

Hydra adds interaction to the mix. It describes exactly how to post a ticket reservation.

So I want to add json-ld information to json objects serialized from my Java beans.

Java beans have no knowledge about the meaning of their bean properties and they do not know what they represent in the real world.

In the simplest possible case I want to design my json objects so that they can be understood by others based on schema.org. By simply calling my json transfer class Person and letting it have an attribute name, I want to get a publicly understandable json object, like this:

    @Test
    public void testDefaultVocabIsRendered() throws Exception {

        class Person {
            private String name = "Dietrich Schulten";

            public String getName() {
                return name;
            }
        }

        mapper.writeValue(w, new Person());
    }

The corresponding json-ld object, written by hydra-java:

{
  "@context": {
    "@vocab": "http://schema.org/"
  },
  "@type": "Person",
  "name": "Dietrich Schulten"
}

Note that I do not bind my clients to a server type Person. Rather, client and server are talking about the thing Person as it is known and recognized by all major search engines.

For a more expressive example consider the json-ld example of MusicEvent, which shows how a ticket offering could look like.

In a more complex scenario I want to use my own attribute names and object design and still be able to use schema.org or other vocabs to describe their meaning. In json-ld I can. See below for a listing of vocabularies.

First Steps

It is currently possible to render responses from a spring-hateoas service based on Spring MVC with various message converters.

Look into the sample configuration to see how you can set up the hydra message converter, but also the XHTML message converter and the Siren message converter with Spring MVC. The tests in JacksonHydraSerializerTest demonstrate the usage of @Vocab, @Expose and @Term.

Features of hydra-spring

The conversion of a spring-hateoas Resource to hydra does the following:

  • renders a spring-hateoas List<Link> in a Resource<T> in json-ld style

  • renders spring-hateoas Resources<T> as hydra:Collection. If you use this feature, make sure you have a @Term(define = "hydra", as = "http://www.w3.org/ns/hydra/core#") annotation in your context.

  • renders spring-hateoas PagedResources<T> as hydra:Collection with a hydra:PartialCollectionView. If you use this feature, make sure you have a @Term(define = "hydra", as = "http://www.w3.org/ns/hydra/core#") annotation in your context.

  • renders response with "@vocab" : "http://schema.org/" by default, a different @vocab can be defined on a class or package using the @Vocab annotation.

  • supports vocabularies in addition to the default vocabulary via terms in the @context. Use @Term in conjunction with @Terms on a class or package for this.

  • renders @type based on the Java class name by default, a vocabulary class can be produced instead using @Expose on the Java class.

  • renders attributes assuming that the attribute name is a property in the default vocab defined by @vocab. In other words, it renders an offers member as "offers" on a json-ld object with a context defining "@vocab" : "http://schema.org", so that you end up with "http://schema.org/offers" as linked data name for your offers member. To map a custom attribute name such as foo to an existing property in the default vocab or other vocabs use @Expose on the attribute and a term will be created in @context which maps your attribute to the vocab property you set as value of @Expose.

  • renders Java enums assuming that an enum value name is an enumerated value defined by the default vocab. In json-ld it is not only possible to have attribute names, but also attribute values that have linked data names. The idiom to express that is "@type" : "@vocab". An example of this is OnSitePickup, which is an enum value for the property availableDeliveryMethod. If your Java enum value is ON_SITE_PICKUP, it matches the vocab value of OnSitePickup. It will be rendered as ON_SITE_PICKUP and hydra-java will add the necessary definition to the context which makes it clear that ON_SITE_PICKUP is actually http://schema.org/OnSitePickup. If your Java enum value has a different name than the vocab value, use @Expose on the enum value to get a correct representation in the context. Note that you can also expose an enum value from a different vocabulary such as GoodRelations, see below.

As of version 0.2.0 hydra-java supports hydra:collection, hydra:operation and hydra:IriTemplate as well as reversed terms. To make this possible, you must use the linkTo and methodOn methods of AffordanceBuilder as a drop-in replacement for ControllerLinkBuilder. Templated links created by ControllerLinkBuilder will at least be rendered as IriTemplates, but only with limited information about the template variables.

Furthermore, if you use these hydra features, make sure you have a @Term(define = "hydra", as = "http://www.w3.org/ns/hydra/core#") annotation in your context.

  • renders a link to a remote collection as hydra:collection. If you define the affordance to the remote collection with AffordanceBuilder.rel(), the remote collection gets a hydra:subject in its manages block, whereas if you define it with reverseRel() you get a hydra:object. To learn more about this design, consider the article Collection Design in the hydra-cg wiki.

  • renders a templated link as hydra:IriTemplate. Method parameters can be annotated with @Expose to assign them a property URI, otherwise the variable name will be shown as a term in the current vocab. If you create a link with AffordanceBuilder’s linkTo-method facilities and you pass null for arguments annotated with @PathVariable or @RequestParam, it will automatically become a templated link with variables for the null arguments.

  • renders a link to method handlers for any combination of GET, POST, PUT, PATCH and DELETE as hydra:operation. In order to express that multiple HTTP methods can be invoked on the same resource, use the and() method of AffordanceBuilder. See below for an example.

  • renders a single, manually created, non-templated Link or Affordance in json-ld style.

  • renders a POJO method parameter annotated with @RequestBody as expected rdfs:subClassOf. Use @Expose on the POJO class for a custom identifier. The setter methods on the bean appear as hydra:supportedProperty, and you can annotate them with @Expose to give them a semantic identifier. Again see below for an example.

  • uses certain schema.org facilities to describe expected request bodies. For this we need schema.org either as @vocab or as a schema: term. If you do not use schema.org as @vocab, make sure you have a @Term(define = "schema", as = "http://schema.org/") in the context.

Examples

Designing a Hydra API

See my article Restbucks with Hydra for an example of an ordering flow.

Live Demo

Use a ReST client to access a Sample Events API to see the artifact hydra-sample at work. There is also a Sample Shop which demonstrates the ideas from the Restbucks with Hydra article. OpenShift sometimes completely shuts down the container, please try several times if you run into server errors when first accessing the sample. As an alternative, @damnhandy has provided hydra-springboot.

Browsers will show the html representation of the API by default, which uses the XhtmlResourceMessageConverter. Sending Accept: application/ld+json will get you hydra, but application/json or application/hal+json work as well. When you POST or PUT, make sure you add a Content-Type header matching your request.

Exposing Java Bean Attributes

Assuming a Java enum whose enum values are exposed as values from GoodRelations and which appears on an Offer object with GoodRelations term:

The example shows a Java enum named `BusinessFunctionˋ whose enum values are exposed as values from GoodRelations. The enum appears on an Offer object with a GoodRelations term:

    enum BusinessFunction {
        @Expose("gr:LeaseOut")
        RENT,
        @Expose("gr:Sell")
        FOR_SALE,
        @Expose("gr:Buy")
        BUY
    }

    @Term(define = "gr", as = "http://purl.org/goodrelations/v1#")
    class Offer {
        public BusinessFunction businessFunction;
        ...
    }

The json-ld output written by hydra-java makes the GoodRelations url known under the shorthand gr, says that the businessFunction property contains values defined by a vocabulary and maps the Java enum value RENT to its linked data name "gr:LeaseOut".

{
    "@context": {
      "@vocab": "http://schema.org/",
      "gr": "http://purl.org/goodrelations/v1#",
      "businessFunction": {"@type": "@vocab"},
      "RENT": "gr:LeaseOut"
    },
    "@type": "Offer",
    "businessFunction": "RENT"
}

A hypermedia affordance is a rich hyperlink. That means, it not only contains a URI or a URITemplate, but also information about the usage of the URI, such as supported http methods and expected parameters. The term 'hypermedia affordance' is a neologism made popular by Mike Amundsen, following an earlier reference in A little REST and Relaxation by Roy Fielding. A hydra-java Affordance can be used to render media-types which support this kind of information: first and foremost hydra, but it is quite easy to add message converters for other media types once the basic information is available.

Version 0.2.0 provides an AffordanceBuilder class which is a drop-in replacement for the spring-hateoas ControllerLinkBuilder.

The AffordanceBuilder does not depend on hydra or json-ld. It lives in the standalone jar spring-hateoas-ext and can also be used to render other media types than json-ld. It has support for all HAL link attributes when rendered as HAL, and can also be rendered as Siren or XHtml using message converters from spring-hateoas-ext.

See Maven Support for the maven coordinates of spring-hateoas-ext.

Use the AffordanceBuilder to build Affordance instances which inherit from the spring-hateoas Link but add the following traits to it:

  • Full support for all attributes of a http Link header as described by the web linking rfc 5988

  • Support for templated link headers as described by the Link-Template Header Internet draft

  • Improved creation of link templates. You can use the linkTo-methodOn technique to create templated links to handler methods. By simply leaving a parameter undefined (null) in a methodOn sample call, a template variable will be applied to your link.

  • Facility to chain several method invocations on the same resource. If the same link is used to PUT and DELETE a resource, use AffordanceBuilder.and() to add both method handlers to the affordance.

  • Has action descriptors with information about http methods and expected request data. Based on reflection and a minimal set of annotations it is possible to render forms-like affordances with quite precise information about expected input.

Use the enhanced builder API of AffordanceBuilder to add more link params than allowed by Link:

AffordanceBuilder.linkTo(methodOn(Foo.class).getBars()).rel("bars") // rel() instead of withRel()
   .withType("text/html")
   .withLinkParam("name", "red-bar") // adding HAL name attribute
   .build();

In the following we use AffordanceBuilder to add a self rel that can be used with GET, PUT and DELETE to an event bean. First we wrap the event into a Resource so we can add affordances to it. Then we use the linkTo-methodOn technique three times to describe that the self rel can be used to get, update and delete the event.

    import static de.escalon.hypermedia.spring.AffordanceBuilder.linkTo;
    import static de.escalon.hypermedia.spring.AffordanceBuilder.methodOn;

    @Controller
    @RequestMapping("/events")
    public class EventController {

        @RequestMapping(value = "/{eventId}", method = RequestMethod.GET)
        public @ResponseBody Resource<Event> getEvent(@PathVariable Integer eventId) {
            // get the event from some backend, then:
            Resource<Event> eventResource = new Resource<Event>(event);

            // using AffordanceBuilder.linkTo and AffordanceBuilder.methodOn
            // instead of ControllerLinkBuilder methods
            eventResource.add(linkTo(methodOn(EventController.class)
                    .getEvent(event.id))
                .and(linkTo(methodOn(EventController.class) // 2nd action with .and
                    .updateEvent(event.id, event)))
                .and(linkTo(methodOn(EventController.class) // 3rd action with .and
                    .deleteEvent(event.id)))
                .withSelfRel());
            return eventResource;
        }

        @RequestMapping(value = "/{eventId}", method = RequestMethod.GET)
        public @ResponseBody Resource<Event> getEvent(@PathVariable Integer eventId) {
            ...
        }


        @RequestMapping(value = "/{eventId}", method = RequestMethod.PUT)
        public ResponseEntity<Void> updateEvent(@PathVariable int eventId, @RequestBody Event event) {
            ...
        }

        @RequestMapping(value = "/{eventId}", method = RequestMethod.DELETE)
        public ResponseEntity<Void> deleteEvent(@PathVariable int eventId) {
            ...
        }
    }

    public class Event {
        public final int id;
        public final String performer;
        public final String location;
        private EventStatusType eventStatus;
        private String name;

        public Event(int id, String performer, String name, String location, EventStatusType eventStatus) {
            ...
        }

        public void setEventStatus(EventStatusType eventStatus) {
            this.eventStatus = eventStatus;
        }
    }

When rendered with the HydraMessageConverter, the resulting json-ld event object has the corresponding GET, PUT and DELETE operations. The PUT operation expects an Event with a property eventStatus. By default, writable properties (with a setter following the JavaBean conventions) are rendered as hydra:supportedProperty. The URI to be used by the operations is the @id of the object that has a hydra:operation.

   {
      "@type": "Event",
      "@id": "http://localhost/events/1",
      "performer": "Walk off the Earth",
      "location": "Wiesbaden",
      "name": "Gang of Rhythm Tour",
      "eventStatus" : "EVENT_SCHEDULED",
      "hydra:operation": [
         {
             "hydra:method": "GET"
         },
         {
             "hydra:method": "PUT",
             "hydra:expects":
             {
                 "@type": "Event",
                 "hydra:supportedProperty": [
                     {
                         "hydra:property": "eventStatus",
                         "hydra:required": "true",
                         "readonlyValue": false
                     },
                     {
                         "hydra:property": "location",
                         "defaultValue": "Wiesbaden",
                         "readonlyValue": false
                     },
                     ... other properties required for a replacing PUT
                 ]
             }
         },
         {
             "hydra:method": "DELETE"
         }
     ]
   }

Specifying Property Value Requirements (from V. 0.2.0)

Now let us tell the client a range of possible values for a property. We want to allow clients to add reviews for the work performed at an event. For this, we add a Resource<CreativeWork> to the Event, so that we can define an affordance on the creative work which allows clients to send reviews.

  public class Event {
      ...
      private final Resource<CreativeWork> workPerformed;

      public Resource<CreativeWork> getWorkPerformed() {
          return workPerformed;
      }
      ...
  }

  // in EventController:
  @RequestMapping(value = "/{eventId}", method = RequestMethod.GET)
  public @ResponseBody Resource<Event> getEvent(@PathVariable Integer eventId) {

      // with an event from backend do this:

      event.getWorkPerformed() //  <-- must be a Resource<CreativeWork>
          .add(linkTo(methodOn(ReviewController.class) // <-- must use AffordanceBuilder.linkTo here
              .addReview(event.id, new Review(null, new Rating(3)))) // <-- default ratingValue 3
              .withRel("review"));
      ...
  }

  @Controller
  @RequestMapping("/reviews")
  public class ReviewController {

      @RequestMapping(value = "/events/{eventId}", method = RequestMethod.POST)
      public ResponseEntity<Void> addReview(@PathVariable int eventId, @RequestBody Review review) {
          // add review and return 201 Created
      }
  }

We expect that clients post a Review with a review body and a rating. The review body and the rating value have input constraints, so we annotate the method setReviewBody with @Input(pattern=".{10,}") and setRatingValue with @Input(min = 1, max = 5, step = 1), as shown below.

  public class Rating {
      private String ratingValue;

      @JsonCreator
      public Rating(@JsonProperty("ratingValue") Integer ratingValue) {
        ..
      }


      public void setRatingValue(@Input(min = 1, max = 5, step = 1) String ratingValue) {
          this.ratingValue = ratingValue;
      }
  }


  public class Review {

      private String reviewBody;
      private Rating reviewRating;

      @JsonCreator
      public Review(@JsonProperty("reviewBody") String reviewBody,
          @JsonProperty("reviewRating") Rating reviewRating) {
             ...
      }

      public void setReviewBody(@Input(pattern=".{10,}") String reviewBody) {
        ...
      }

      public void setReviewRating(Rating rating) {
          this.reviewRating = rating;
      }
  }

In the resulting json-ld we use schema.org’s PropertyValueSpecification to express the input constraints minValue, maxValue, stepValue and valuePattern, as well as defaultValue containing the rating value 3 that was passed to the sample method invocation with methodOn. Note that the creative work has a review attribute now, although the CreativeWork pojo has no such property. It appears because we added a rel review to the workPerformed resource.

Right now it is not possible to specify a list of expected values, neither with hydra nor with schema:PropertyValueSpecification. If you are interested in that, look into #82 Add support for allowed literals and allowed individuals and participate in the discussion in the Hydra-CG mailing list.

{
    "@context":
    {
        "@vocab": "http://schema.org/",
        "hydra": "http://www.w3.org/ns/hydra/core#",
        "eventStatus":
        {
            "@type": "@vocab"
        },
        "EVENT_SCHEDULED": "EventScheduled"
    },
    "@type": "Event",
    "performer": "Walk off the Earth",
    "location": "Wiesbaden",
    "eventStatus": "EVENT_SCHEDULED",
    "workPerformed": {
        "@type": "CreativeWork",
        "name": "Gang of Rhythm Tour",
        "review": {
            "@id": "http://localhost:8210/webapp/hypermedia-api/reviews/events/1",
            "hydra:operation": [
                {
                    "@type": "ReviewAction",
                    "hydra:method": "POST",
                    "hydra:expects": {
                        "@type": "Review",
                        "hydra:supportedProperty": [
                            {
                                "@type": "PropertyValueSpecification",
                                "hydra:property": "reviewBody",
                                "valuePattern": ".{10,}"
                            },
                            {
                                "hydra:property": "reviewRating",
                                "rangeIncludes": {
                                    "@type": "Rating",
                                    "hydra:supportedProperty": [
                                        {
                                            "@type": "PropertyValueSpecification",
                                            "hydra:property": "ratingValue",
                                            "defaultValue": 3,
                                            "maxValue": 5,
                                            "minValue": 1,
                                            "stepValue": 1
                                        }
                                    ]
                                }
                            }
                        ]
                    }
                }
            ]
        }
    }
}

If an expected property on a request object holds a nested json object in turn, hydra-java will render it following a proposal from Hydra-CG Issue 26 using schema:rangeIncludes. The fact that this issue is not resolved yet is the main reason why hydra-java 0.2.0 is an alpha release. So be especially wary that changes are likely for the way hydra-java prescribes nested properties.

Rendering other media types (from V. 0.2.0-alpha8)

Clients should be able to request a media-type they understand by means of content negotiation. Following this principle, the spring-hateoas-ext package provides the foundation to render hypermedia types which describe expected requests - not only as json-ld, but also as other media types.

XhtmlResourceMessageConverter

The XhtmlResourceMessageConverter is the second message converter in hydra-java which makes use of affordances built by AffordanceBuilder.

If you add a JsonLdDocumentationProvider on the converter, it will render bean attributes as hyperlinks which point to their documentation on schema.org or other vocabularies, provided that your java beans are annotated with the necessary information.

The xhtml response renders bootstrap conforming markup, you can add bootstrap css as shown below, or your own stylesheets.

@Configuration
@EnableWebMvc
public class Config extends WebMvcConfigurerAdapter {
    ...
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(halConverter());
        converters.add(xhtmlMessageConverter());
        converters.add(jsonConverter());
    }

    private HttpMessageConverter<?> xhtmlMessageConverter() {
        XhtmlResourceMessageConverter xhtmlResourceMessageConverter = new XhtmlResourceMessageConverter();
        xhtmlResourceMessageConverter.setStylesheets(
                Arrays.asList(
                        "https://maxcdn.bootstrapcdn.com/bootstrap/3.3.4/css/bootstrap.min.css"
                ));
        xhtmlResourceMessageConverter.setDocumentationProvider(new JsonLdDocumentationProvider());
        return xhtmlResourceMessageConverter;
    }
    ...
}

To make the API browsable, PUT and DELETE are tunneled through POST. This is necessary because the HTML media type does not support PUT or DELETE, the browser cannot handle a form which has other methods than GET or POST. Spring-MVC has a servlet filter which makes tunneling easy. The web.xml of the hydra-sample service shows how to enable that filter:

    <filter>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>HiddenHttpMethodFilter</filter-name>
        <servlet-name>hypermedia-api</servlet-name>
    </filter-mapping>

SirenMessageConverter (from V. 0.2.0-beta5)

The SirenMessageConverter renders Spring Hateoas Responses as Siren messages, using the media type application/vnd.siren+json.

  • maps a plain Spring Hateoas Link to an embedded link or navigational link.

  • a templated link becomes a Siren GET action with named siren fields for the template query variables

  • in order to produce more expressive Siren actions, use the linkTo-methodOn idiom of AffordanceBuilder to point to your methods, as shown above for the sample EventController in the section AffordanceBuilder.

  • possible values found by AffordanceBuilder are treated as checkbox or radio button fields, following the technique discussed in the Siren group.

  • field types can be defined via the value of the @Input annotation on method parameters (e.g. @Input(Type.DATE)).

  • nested Resource objects are shown as embedded representations

  • distinguishes navigational and embedded links by a default list of navigational rels. This list can be customized via SirenMessageConverter.addNavigationalRels.

  • for sub-entities the property name is used as relation name. The Siren class name is derived from the Java class name. The rel names can be customized using a DocumentationProvider implementation, e.g. the JsonLdDocumentationProvider from hydra-jsonld will make use of @Expose and @Vocab annotations on your response bean packages.

  • relies on XhtmlMessageConverter to process incoming form-urlencoded requests and on MappingJackson2HttpMessageConverter for json requests.

The Siren output for the sample EventController above is shown below. Note that the JsonLdDocumentationProvider has created the link relation type http://schema.org/workPerformed. One could also use the UrlPrefixDocumentationProvider for simple URL prefixing.

{
    "class": [
        "event"
    ],
    "properties": {
        "performer": "Walk off the Earth",
        "eventStatus": "EVENT_SCHEDULED",
        "location": "Wiesbaden"
    },
    "entities": [
        {
            "class": [
                "creativeWork"
            ],
            "rel": [
                "http://schema.org/workPerformed"
            ],
            "properties": {
                "name": "Gang of Rhythm Tour"
            },
            "actions": [
                {
                    "name": "addReview",
                    "method": "POST",
                    "href": "http://example.com/webapp/hypermedia-api/reviews/events/1",
                    "fields": [
                        {
                            "name": "reviewBody",
                            "type": "text"
                        },
                        {
                            "name": "reviewRating.ratingValue",
                            "type": "number",
                            "value": "3"
                        }
                    ]
                }
            ]
        }
    ],
    "actions": [
        {
            "name": "updateEvent",
            "method": "PUT",
            "href": "http://example.com/webapp/hypermedia-api/events/1",
            "fields": [
                {
                    "name": "location",
                    "type": "text",
                    "value": "Wiesbaden"
                },
                {
                    "name": "eventStatus",
                    "type": "radio",
                    "value": [
                        {
                            "value": "EVENT_CANCELLED"
                        },
                        {
                            "value": "EVENT_POSTPONED"
                        },
                        {
                            "value": "EVENT_SCHEDULED",
                            "selected": true
                        },
                        {
                            "value": "EVENT_RESCHEDULED"
                        }
                    ]
                }
                ... other properties required for a replacing PUT
            ]
        },
        {
            "name": "deleteEvent",
            "method": "DELETE",
            "href": "http://example.com/webapp/hypermedia-api/events/1"
        }
    ],
    "links": [
        {
            "rel": [
                "self"
            ],
            "href": "http://example.com/webapp/hypermedia-api/events/1"
        }
    ]
}

Maven Support

The latest Maven releases of hydra-java are in Maven central. These are the maven coordinates for hydra-spring.

<dependency>
  <groupId>de.escalon.hypermedia</groupId>
  <artifactId>hydra-spring</artifactId>
  <version>0.4.1</version>
</dependency>

If you only want to use AffordanceBuilder or the XhtmlResourceMessageConverter and SirenMessageConverter without the json-ld dependencies, use spring-hateoas-ext alone:

<dependency>
  <groupId>de.escalon.hypermedia</groupId>
  <artifactId>spring-hateoas-ext</artifactId>
  <version>0.4.1</version>
</dependency>

Vocabularies

What if schema.org is not sufficient? On Linked Open Vocabularies you can search for terms in other vocabularies. Another option is to propose an addition to schema.org.

If you are unsure which vocab to use, ask on the hydra mailing list.

What’s new

0.4.0

  • Updated to current spring-hateoas with Spring 4, no longer compatible with Spring 3. Please use 0.3.x if you need Spring 3.

0.3.1

  • PartialUriTemplate no longer wrongly rearranges url having unexpanded simple string variables in the query

0.3.0

  • extraction of ActionDescriptor and ActionInputParameter interfaces, coordinating with HDIV to get forms into spring-hateoas

  • optimization of json-ld output: do not repeat terms which are in the parent context already

  • simple feature to use query parameters mapped to parameter bean or parameter Map annotated with @Input rather than single RequestParam arguments. Right now, it can only be used to build a UriTemplate, no description for the template variables is available yet. Use @Input(include=…​, exclude=…​) to filter applicable bean properties or describe expected Map values. The UriTemplate for such an affordance is available via Affordance.getUriTemplateComponents().toString(), but not via Affordance.toString() to keep an Affordance created via AffordanceBuilder compatible with a Link created by ControllerLinkBuilder.

  • Affordance now has a type property and unwraps extension link params when rendered as JSON, which e.g. allows to use link attributes of HAL (type, name, deprecation etc.) which are not present in the basic Link class

Acknowledgements

I would like to thank Mike Amundsen, Stu Charlton, Jon Moore, Jørn Wildt, Mike Kelly, Markus Lanthaler, Gregg Kellog and Manu Sporny for their inspiration and for valuable comments along the way. Also thanks to Oliver Gierke who has been accepting some of my pull requests to spring-hateoas.

hydra-java's People

Contributors

ceefour avatar dschulten avatar fkleon avatar nibe avatar sotty avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

hydra-java's Issues

SpringBoot AutoConfiguration support and Hydra SpringBoot Starter

I've been messing around with Hydra-Java and SpringBoot and have the two working. I have taken the sample project and refactored it into a SpringBoot application:

https://github.com/damnhandy/hydra-springboot

It'd be useful to have a SpringBoot auto configuration module that would provision the correct types using something like an @EnableHydraServer annotation. Taking it a step further, one could also declare which mime types are enabled by specifying them in the annotation value:

@SpringBootApplication
@EnableHydraServer({HYDRA,SIREN,UBER})
public class HydraSampleServiceApplication { ...

I'd be happy to contribute this functionality, but I'm not sure that the core project is the best place for this functionality to live as those not using SpringBoot will not want to deal with those dependencies.

java.lang.NullPointerException at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.getTerms(JacksonHydraSerializer.java:252)

The NullPointerException should give an explanation what is expected.

The exception is thrown here:

        for (Field field : fields) {
            final Expose fieldExpose = field.getAnnotation(Expose.class);
            if (Enum.class.isAssignableFrom(field.getType())) {
                Map<String, String> map = new LinkedHashMap<String, String>();
                termsMap.put(field.getName(), map);
                if (fieldExpose != null) {
                    map.put(AT_ID, fieldExpose.value());
                }
                map.put(AT_TYPE, AT_VOCAB);
                final Enum value = (Enum)field.get(bean);

                // -------------- EXCEPTION HERE ------------
                final Expose enumValueExpose = getAnnotation(value.getClass().getField(value.name()), Expose.class);


                // TODO redefine actual enum value to exposed on enum value definition
                if (enumValueExpose != null) {
                    termsMap.put(value.toString(), enumValueExpose.value());
                } else {

With following models:

Product.java:

public class Product extends Thing<Product> implements IProduct {

    public static final long serialVersionUID = 1L;

    public OrganizationOrBrand brand;
    public Organization manufacturer;
    public String sku;
    public String barcode;
    public CurrencyUnit priceCurrency;
    public BigDecimal price;
    public DecimalMeasure<Mass> weight;
    public DecimalMeasure<Length> depth;
    public DecimalMeasure<Length> width;
    public DecimalMeasure<Length> height;
    public IColor color;
    public BigDecimal productionCost;
    public InventoryManagement inventoryManagement;
    public InventoryPolicy inventoryPolicy;
    public DecimalMeasure<Quantity> inventoryLevel;
    @Expose("http://www.soluvas.org/commerceplug/1.0#inventoryOnHand")
    public DecimalMeasure<Quantity> inventoryOnHand;
    @JsonInclude(Include.NON_EMPTY)
    public final List<OptionType> optionTypes = new ArrayList<>();
    @JsonInclude(Include.NON_EMPTY)
    public final List<Product> variants = new ArrayList<>();
    protected final List<Offer> offers = new ArrayList<>();
    public String serialNumber;
    @JsonProperty("additionalProperty") @JsonInclude(Include.NON_EMPTY)
    public final List<PropertyValue<?>> additionalProperties = new ArrayList<>();

    @Override
    public OrganizationOrBrand getBrand() {
        return brand;
    }

    @Override
    public void setBrand(OrganizationOrBrand brand) {
        this.brand = brand;
    }

    public Product withBrand(OrganizationOrBrand brand) {
        this.brand = brand;
        return this;
    }

    @Override
    public Organization getManufacturer() {
        return manufacturer;
    }

    @Override
    public void setManufacturer(Organization manufacturer) {
        this.manufacturer = manufacturer;
    }

    public Product withManufacturer(Organization manufacturer) {
        this.manufacturer = manufacturer;
        return this;
    }

    @Override
    public String getSku() {
        return sku;
    }

    @Override
    public void setSku(String sku) {
        this.sku = sku;
    }

    public Product withSku(String sku) {
        this.sku = sku;
        return this;
    }

    @Override
    public String getBarcode() {
        return barcode;
    }

    @Override
    public void setBarcode(String barcode) {
        this.barcode = barcode;
    }

    public Product withBarcode(String barcode) {
        this.barcode = barcode;
        return this;
    }

    @Override
    public CurrencyUnit getPriceCurrency() {
        return priceCurrency;
    }

    @Override
    public void setPriceCurrency(CurrencyUnit currency) {
        this.priceCurrency = currency;
    }

    public Product withPriceCurrency(CurrencyUnit currency) {
        this.priceCurrency = currency;
        return this;
    }

    @Override
    public BigDecimal getPrice() {
        return price;
    }

    @Override
    public void setPrice(BigDecimal price) {
        this.price = price;
    }

    public Product withPrice(BigDecimal price) {
        this.price = price;
        return this;
    }

    @Override
    public DecimalMeasure<Mass> getWeight() {
        return weight;
    }

    @Override
    public void setWeight(DecimalMeasure<Mass> weight) {
        this.weight = weight;
    }

    public Product withWeight(DecimalMeasure<Mass> weight) {
        this.weight = weight;
        return this;
    }

    @Override
    public DecimalMeasure<Length> getDepth() {
        return depth;
    }

    @Override
    public void setDepth(DecimalMeasure<Length> depth) {
        this.depth = depth;
    }

    public Product withDepth(DecimalMeasure<Length> depth) {
        this.depth = depth;
        return this;
    }

    @Override
    public DecimalMeasure<Length> getWidth() {
        return width;
    }

    @Override
    public void setWidth(DecimalMeasure<Length> width) {
        this.width = width;
    }

    public Product withWidth(DecimalMeasure<Length> width) {
        this.width = width;
        return this;
    }

    @Override
    public DecimalMeasure<Length> getHeight() {
        return height;
    }

    @Override
    public void setHeight(DecimalMeasure<Length> height) {
        this.height = height;
    }

    public Product withHeight(DecimalMeasure<Length> height) {
        this.height = height;
        return this;
    }

    @Override
    public IColor getColor() {
        return color;
    }

    @Override
    public void setColor(IColor color) {
        this.color = color;
    }

    public Product withColor(IColor color) {
        this.color = color;
        return this;
    }

    @Override
    public BigDecimal getProductionCost() {
        return productionCost;
    }

    @Override
    public void setProductionCost(BigDecimal productionCost) {
        this.productionCost = productionCost;
    }

    public Product withProductionCost(BigDecimal productionCost) {
        this.productionCost = productionCost;
        return this;
    }

    public InventoryManagement getInventoryManagement() {
        return inventoryManagement;
    }

    public void setInventoryManagement(InventoryManagement inventoryManagement) {
        this.inventoryManagement = inventoryManagement;
    }

    public Product withInventoryManagement(InventoryManagement inventoryManagement) {
        this.inventoryManagement = inventoryManagement;
        return this;
    }

    public InventoryPolicy getInventoryPolicy() {
        return inventoryPolicy;
    }

    public void setInventoryPolicy(InventoryPolicy inventoryPolicy) {
        this.inventoryPolicy = inventoryPolicy;
    }

    public Product withInventoryPolicy(InventoryPolicy inventoryPolicy) {
        this.inventoryPolicy = inventoryPolicy;
        return this;
    }

    @Override
    public DecimalMeasure<Quantity> getInventoryLevel() {
        return inventoryLevel;
    }

    @Override
    public void setInventoryLevel(DecimalMeasure<Quantity> inventoryLevel) {
        this.inventoryLevel = inventoryLevel;
    }

    public Product withInventoryLevel(DecimalMeasure<Quantity> inventoryLevel) {
        this.inventoryLevel = inventoryLevel;
        return this;
    }

    @Override
    public DecimalMeasure<Quantity> getInventoryOnHand() {
        return inventoryOnHand;
    }

    @Override
    public void setInventoryOnHand(DecimalMeasure<Quantity> inventoryOnHand) {
        this.inventoryOnHand = inventoryOnHand;
    }

    public Product withInventoryOnHand(DecimalMeasure<Quantity> inventoryOnHand) {
        this.inventoryOnHand = inventoryOnHand;
        return this;
    }

    @Override
    public List<OptionType> getOptionTypes() {
        return optionTypes;
    }

    public Product addOptionType(OptionType optionType) {
        getOptionTypes().add(optionType);
        return this;
    }

    @Override
    public List<Product> getVariants() {
        return variants;
    }

    public Product addVariant(Product variant) {
        getVariants().add(variant);
        return this;
    }

    @JsonInclude(Include.NON_EMPTY)
    public List<Offer> getOffers() {
        return offers;
    }

    public Product addOffer(Offer offer) {
        getOffers().add(offer);
        return this;
    }

    @Override
    public String getSerialNumber() {
        return serialNumber;
    }

    @Override
    public void setSerialNumber(String serialNumber) {
        this.serialNumber = serialNumber;
    }

    public Product withSerialNumber(String serialNumber) {
        this.serialNumber = serialNumber;
        return this;
    }

    @Override
    public List<PropertyValue<?>> getAdditionalProperties() {
        return additionalProperties;
    }

    public Product withAdditionalProperty(PropertyValue<?> propertyValue) {
        getAdditionalProperties().add(propertyValue);
        return this;
    }

}

Thing.java:

@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({"id", "name", "description", "image", "url",
    "additionalType", "alternateName", "potentialAction", "sameAs"})
@SuppressWarnings("unchecked")
public class Thing<C extends IThing> extends ResourceSupport 
    implements IThing, Serializable {

    public static final long serialVersionUID = 1L;

    public String id;
    @JsonProperty("additionalType") @JsonInclude(Include.NON_EMPTY)
    public final List<String> additionalTypes = new ArrayList<>();
    public String alternateName;
    public String description;
    public ImageObject image;
    public String name;
    public Action potentialAction;
    public String sameAs;
    public String url;
    @JsonIgnore
    public final Map<String, Object> customProperties = new LinkedHashMap<>();

    @Override
    @JsonProperty("id")
    public String getThingId() {
        return id;
    }

    @Override
    public void setThingId(String id) {
        this.id = id;
    }

    public C withThingId(String id) {
        this.id = id;
        return (C) this;
    }

    @Override
    public List<String> getAdditionalTypes() {
        return additionalTypes;
    }

    public C addAdditionalType(String additionalType) {
        getAdditionalTypes().add(additionalType);
        return (C) this;
    }

    @Override
    public String getAlternateName() {
        return alternateName;
    }

    public void setAlternateName(String alternateName) {
        this.alternateName = alternateName;
    }

    @Override
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public C withDescription(String description) {
        this.description = description;
        return (C) this;
    }

    @Override
    public ImageObject getImage() {
        return image;
    }

    public void setImage(ImageObject image) {
        this.image = image;
    }

    @Override
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public C withName(String name) {
        this.name = name;
        return (C) this;
    }

    @Override
    public Action getPotentialAction() {
        return potentialAction;
    }

    public void setPotentialAction(Action potentialAction) {
        this.potentialAction = potentialAction;
    }

    @Override
    public String getSameAs() {
        return sameAs;
    }

    public void setSameAs(String sameAs) {
        this.sameAs = sameAs;
    }

    @Override
    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    @JsonAnyGetter
    public Map<String, Object> getCustomProperties() {
        return this.customProperties;
    }

    @JsonAnySetter
    public void setCustomProperty(String name, Object value) {
        this.customProperties.put(name, value);
    }

    public C withCustomProperty(String name, Object value) {
        this.customProperties.put(name, value);
        return (C) this;
    }

}

Error:

com.fasterxml.jackson.databind.JsonMappingException: java.lang.NullPointerException
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:125)
    at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:2866)
    at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:2323)
    at id.co.bippo.product.rs.commerceplug.ProductOrServiceImplJsonLdTest.productOrService(ProductOrServiceImplJsonLdTest.java:45)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.RuntimeException: java.lang.NullPointerException
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.serializeContext(JacksonHydraSerializer.java:199)
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.serialize(JacksonHydraSerializer.java:105)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:114)
    ... 27 more
Caused by: java.lang.NullPointerException
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.getTerms(JacksonHydraSerializer.java:252)
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.serializeContext(JacksonHydraSerializer.java:149)
    ... 29 more

Using hydra-java without Spring

It would be nice to use hydra-java to render responses from a service that uses plain Jax-RS, with Jackson, for example. In that sense, I found out that this can be accomplished if we remove the dependencies on spring from the class 'JacksonHydraModule'. So, If you like, I can send a pull request containing the refactoring of this class, splitting it in two, one with the current code without the spring dependencies and the other as a subclass of the first plus the dependencies on spring.
What do you think?

Spike for hal-forms

FInd out if we can re-use information collected when building the hal response to create hal-forms response.

MediaType constant

public static final String APPLICATION_JSONLD_STR = "application/ld+json";
public static final MediaType APPLICATION_JSONLD = MediaType.parseMediaType(APPLICATION_JSONLD_STR);

this should be a public constant somewhere accessible... Or better yet, should propose inclusion of this constant into https://github.com/spring-projects/spring-hateoas

The String is useful for @RequestMapping.

I wonder if should propose the other MediaType constants as well (Collections+JSON, SIREN, JSONAPI) to spring-hateoas?

Consider moving this project to an organization, possibly new name

First of this, this is a fantastic project. I came here looking for Hydra support, and found Über, Siren, and HAL as well. I think this project has legs as it's very powerful.

I'd like to suggest creating a top-level organization for this project. This would make it simpler to include things like example apps, which may not leverage the same dependency set as the core project. For example, a Spring Boot example would be useful, but likely isn't a good fit for inclusion in the core project.

But this leads to another suggestion: given this project's vast support for other hypermedia formats beyond JSON-LD and Hydra, is hydra-java a reasonable name going forward? This project seems to be shaping up to be a general-purpose Hypermedia framework, of which JSON-LD plus Hydra is one such option. Thoughts?

java.lang.IllegalAccessException: Class de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer can not access a member of class ... with modifiers "private"

With the following object :

@Vocab("http://example.com/")
public class Product extends Thing<Product> implements IProduct {

    @JsonInclude(Include.NON_EMPTY)
    private final List<Offer> offers = new ArrayList<>();

    public List<Offer> getOffers() {
        return offers;
    }

    public Product addOffer(Offer offer) {
        getOffers().add(offer);
        return this;
    }

}

hydra-java fails:

com.fasterxml.jackson.databind.JsonMappingException: java.lang.IllegalAccessException: Class de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer can not access a member of class id.co.bippo.product.rs.commerceplug.Offer with modifiers "private" (through reference chain: id.co.bippo.product.rs.commerceplug.Product["offers"]->java.util.ArrayList[0])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:210)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:189)
    at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:213)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:105)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:21)
    at com.fasterxml.jackson.databind.ser.std.AsArraySerializerBase.serialize(AsArraySerializerBase.java:183)
    at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:505)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:639)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeWithType(BeanSerializerBase.java:525)
    at com.fasterxml.jackson.databind.ser.impl.TypeWrappedSerializer.serialize(TypeWrappedSerializer.java:35)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:114)
    at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:2866)
    at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:2323)
    at id.co.bippo.product.rs.commerceplug.ProductOrServiceImplJsonLdTest.productOrService(ProductOrServiceImplJsonLdTest.java:78)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:675)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:192)
Caused by: java.lang.RuntimeException: java.lang.IllegalAccessException: Class de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer can not access a member of class id.co.bippo.product.rs.commerceplug.Offer with modifiers "private"
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.serializeContext(JacksonHydraSerializer.java:199)
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.serialize(JacksonHydraSerializer.java:105)
    at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:100)
    ... 34 more
Caused by: java.lang.IllegalAccessException: Class de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer can not access a member of class id.co.bippo.product.rs.commerceplug.Offer with modifiers "private"
    at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:101)
    at java.lang.reflect.AccessibleObject.slowCheckMemberAccess(AccessibleObject.java:295)
    at java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:287)
    at java.lang.reflect.Field.get(Field.java:384)
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.getTerms(JacksonHydraSerializer.java:251)
    at de.escalon.hypermedia.hydra.serialize.JacksonHydraSerializer.serializeContext(JacksonHydraSerializer.java:149)
    ... 36 more

Curiously, the actual cuplrit is not mentioned in the stacktrace above:

public class Offer extends Thing<Offer> {

    private static final long serialVersionUID = 1L;

    private ItemAvailability availability;

    /**
     * The availability of this item—for example {@link ItemAvailability#IN_STOCK}, {@link ItemAvailability#OUT_OF_STOCK},
     * {@link ItemAvailability#PRE_ORDER}, etc.
     * @return
     */
    public ItemAvailability getAvailability() {
        return availability;
    }

    public void setAvailability(ItemAvailability availability) {
        this.availability = availability;
    }

    public Offer withAvailability(ItemAvailability availability) {
        this.availability = availability;
        return this;
    }

}

Workaround: change private ItemAvailability availability; to public ItemAvailability availability;

Issue with traverson

While using traveson getting below error
java.lang.IllegalStateException: Did not find LinkDiscoverer supporting media type application/vnd.siren+json;charset=UTF-8!

I am using below configuration

@Bean
  public RestOperations restOperations() {
    RestTemplate restTemplate = new RestTemplate();
    restTemplate.setRequestFactory(new HttpComponentsAsyncClientHttpRequestFactory());
    restTemplate.getInterceptors().add(new BasicAuthorizationInterceptor(user, password));
    return restTemplate;
  }

  @Override
  public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    converters.add(sirenMessageConverter());
  }


  @Bean
  public SirenMessageConverter sirenMessageConverter() {
    SirenMessageConverter sirenMessageConverter = new SirenMessageConverter();
    sirenMessageConverter.setSupportedMediaTypes(Collections.singletonList(SIREN_JSON));
    return sirenMessageConverter;
  }

and calling it like

Traverson traverson = new Traverson(URI.create(baseUrl+"/api.json"), SIREN_JSON);
traverson.setRestOperations(restOperations);
ResponseEntity<Entity>  entity = traverson.follow("child").toEntity(Entity.class);

What am I doing wrong, or this extension doesn't add mediaType support for siren with traverson.
Would be nice if you can build an AutoConfigurationClass

I believe to make it work, we need to add the entry in PluginRegistry with MediaType.SIREN_JSON

AffordanceBuilder GET methods not listed

Hello,

I've tried to reference some hydra:operation in my responses, but whatever the Annotations I put on the controller, I can't show them in the operations list.

The Controller Interface :

@Api(value = "hydra", description = "the hydra API")
public interface HydraApi {

    @ApiOperation(value = "", notes = "", response = Void.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "Successfully added device", response = Void.class) })
    
    @RequestMapping(value = "/hydra/device",
        produces = { "application/ld+json" }, 
        method = RequestMethod.POST)
    ResponseEntity<Object> add(@ApiParam(value = "" ,required=true )  @Valid @RequestBody Device body);


    @ApiOperation(value = "", notes = "", response = Void.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "Successfully deleted device", response = Void.class) })
    
    @RequestMapping(value = "/hydra/device",
        produces = { "application/ld+json" }, 
        method = RequestMethod.DELETE)
    ResponseEntity<Object> delete(@ApiParam(value = "" ,required=true )  @Valid @RequestBody Device body);


    @ApiOperation(value = "", notes = "", response = DeviceData.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "Successfully read device data", response = DeviceData.class) })
    
    @RequestMapping(value = "/hydra/device/read/{maker}/{model}/{serial}",
        produces = { "application/ld+json" }, 
        method = RequestMethod.GET)
    ResponseEntity<Object> read(@ApiParam(value = "Maker of the device",required=true ) @PathVariable("maker") String maker,
                                @ApiParam(value = "Model of the device",required=true ) @PathVariable("model") String model,
                                @ApiParam(value = "Serial of the device",required=true ) @PathVariable("serial") String serial);


    @ApiOperation(value = "", notes = "", response = EntryPoint.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "The API endpoints", response = EntryPoint.class) })
    
    @RequestMapping(value = "/hydra",
        produces = { "application/ld+json" }, 
        method = RequestMethod.GET)
    ResponseEntity<Object> rootEntryPoint();

}

The Resource builder :

    public ResponseEntity<Object> process() {
        ResponseEntity ret;

        try {
            Resource<EntryPoint> entryPointResource =
                    new Resource<>(new EntryPoint());

            entryPointResource.add(
                    linkTo(methodOn(HydraApi.class).add(new Device()))
                            .and(linkTo(methodOn(HydraApi.class).delete(new Device())))
                            .and(linkTo(methodOn(HydraApi.class).read("a", "b", "c")))
                            .withSelfRel()
            );
            ret = new ResponseEntity<Resource>(
                    entryPointResource,
                    HttpStatus.OK
            );
        } catch (Exception e) {
            ret = resolveException(e);
        }
        return new ResponseEntity<>(ret.getBody(), ret.getStatusCode());
    }

The response of the Resource builder :

{
  "@context": {
    "@vocab": "http://schema.org/"
  },
  "@type": "EntryPoint",
  "@id": "http://localhost:8080/hydra/device",
  "hydra:operation": [
    {
      "hydra:method": "POST",
      "hydra:expects": {
        "@type": "Device",
        "hydra:supportedProperty": [
          {
            "hydra:property": "serial"
          },
          {
            "hydra:property": "maker"
          },
          {
            "hydra:property": "model"
          }
        ]
      }
    },
    {
      "hydra:method": "DELETE",
      "hydra:expects": {
        "@type": "Device",
        "hydra:supportedProperty": [
          {
            "hydra:property": "serial"
          },
          {
            "hydra:property": "maker"
          },
          {
            "hydra:property": "model"
          }
        ]
      }
    }
  ]
}

Thanks in advance for the answer !

I suppose this project is dead, yes?

Hi dschulten,

I'm relating to #18 .

I'm also very doubtful it was a good decision to entirely rely on Spring HATEOAS for the backing model which is then mapped to Hydra in a hardcoded way. For Links that mapping is done in a seemingly never ending if-else chain in LinkListSerializer. This class alone I consider highly problematic with regards to judging the readiness of this framework to support the adoption of Hydra in the Java ecosystem. Unfortunately hydra-java is the only listed Java implementation on hydra-cg.com, making it look a bit like THE Hydra reference implementation, which it clearly isn't. At least not as long as the actual Hydra model lives in a bunch of highly nested if/else branches with TODOs galore on them. Yes I understand it's all in testing but that's still no excuse to leave it like this.

That's all well and good if hydra-java is just some random Hydra implementation on GitHub serving mostly the creators own needs.
At the same time I'm not sure how many visitors looking for a Java Hydra backend implementation came here, noticed that...

  1. The Hydra vocabulary is not actually modelled, let alone properly exposed.
  2. The project has multiple hard dependencies to Spring. Where I work Spring in general is considered enterprise legacy cruft.
  3. Is seems conceptually opaque and written in a less than expressive code style in its most vital parts.

... and then left immediately.

With that in mind and the observation this project smells all but dead - this seems like a wasted opportunity. I cannot even fork this because it would basically be an 80% rewrite.

Also, what have Siren and Uber to do in a project named hydra-java?

Yes, this is a rant! If you create something, leave it in a prominent place, making it look like a solution to a problem and then just never come back - it will cost other peoples time.

Paxbit

Handle incoming json-ld in expanded form

Right now we only understand json requests whose attributes have a name equivalent on the target java bean. Enhance this so that an incoming json-ld object is mapped based on the expanded form.

Issue generating links off of request mapping in parent class

I am running into a problem generating links off of routes in a specific scenario. I have a controller method in an abstract parent class that has a signature like this:

    @RequestMapping(method = RequestMethod.GET)
    public HttpEntity<CollectionResource> getCollection(
            @RequestParam(name = "offset") Integer offset,
            @RequestParam(name = "size") Integer size,
            @RequestParam(name = "sort") String sort,
            @RequestParam(name = "q") String q,
            @RequestParam(name = "filter") T filter)

...where T is a generic parameter specified by what is subclassing this parent class. In another unrelated resource, I link to the child class using AffordanceBuilder like so:

        Affordance links = AffordanceBuilder.linkTo(methodOn(
                ChildController.class)
                .getCollection(null, null, null, null, null))
                .withRel("resourcerelation");

What gets generated winds up looking like this:

href: "http://localhost:8080/apinull{?offset,size,sort,q,filter}"

...the obvious issue being that while the base url, context root and uri template all get generated properly, the path is coming through as "null".

Let me know if I can give more specific details about this or create a quick example app that demonstrates it. There is a lot more code around this than what I posted that could possibly have an effect.

Using the Affordance Builder with a null HttpServletRequest

When testing code that uses the AffordanceBuilder.java and not using a library like Mockito or MockMvc, since there is no HttpServlet Context, the HttpServletRequest object is null,

  static UriComponentsBuilder getBuilder() {

        HttpServletRequest request = getCurrentRequest();
        ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);

        String forwardedSsl = request.getHeader("X-Forwarded-Ssl");

inserting this line:

  static UriComponentsBuilder getBuilder() {
        if (RequestContextHolder.getRequestAttributes() == null) {
            			return UriComponentsBuilder.fromPath("");
            		}

        HttpServletRequest request = getCurrentRequest();
        ServletUriComponentsBuilder builder = ServletUriComponentsBuilder.fromServletMapping(request);

        String forwardedSsl = request.getHeader("X-Forwarded-Ssl");

Makes it work, as found here : spring-projects/spring-hateoas#592

AffordanceBuilderFactory does not use value of a @RequestParam

I have a controller method with arguments annotated with @RequestParam, like this:

@GetMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView showAsHtml(final @RequestParam("q") String query,
                               final @RequestParam(value = "offset", defaultValue = "0") Long offset,
                               final Locale locale) {
  // do stuff
}

Now I build an Affordance that is an URI template:

AffordanceBuilder.linkTo(AffordanceBuilder.methodOn(MyController.class)
        .showAsHtml(null, null, null))
        .rel("something")
        ...;

The serialized hydra:IriTemplate will look like this:

{
    "@type": "hydra:IriTemplate",
    "hydra:template": "http://localhost:8080/something{?query}",
    "hydra:mapping": [
      {
        "@type": "hydra:IriTemplateMapping",
        "hydra:variable": "query",
        "hydra:required": true,
        "hydra:property": "query"
      }
    ]
  }

So the variable "query" is wrong. It should be "q" as defined by the @RequestParam.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.