├── codecov.yml ├── MAINTAINERS ├── OSSMETADATA ├── gradle.properties ├── .gitignore ├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── example-springboot ├── src │ └── main │ │ ├── resources │ │ └── static │ │ │ ├── vendor │ │ │ └── img │ │ │ │ ├── ajax-loader.gif │ │ │ │ ├── glyphicons-halflings.png │ │ │ │ └── glyphicons-halflings-white.png │ │ │ ├── js │ │ │ ├── hal │ │ │ │ ├── views │ │ │ │ │ ├── documentation.js │ │ │ │ │ ├── properties.js │ │ │ │ │ ├── navigation.js │ │ │ │ │ ├── explorer.js │ │ │ │ │ ├── response.js │ │ │ │ │ ├── response_headers.js │ │ │ │ │ ├── request_headers.js │ │ │ │ │ ├── browser.js │ │ │ │ │ ├── inspector.js │ │ │ │ │ ├── embedded_resources.js │ │ │ │ │ ├── location_bar.js │ │ │ │ │ ├── resource.js │ │ │ │ │ ├── response_body.js │ │ │ │ │ ├── links.js │ │ │ │ │ ├── non_safe_request_dialog.js │ │ │ │ │ ├── embedded_resource.js │ │ │ │ │ └── query_uri_dialog.js │ │ │ │ ├── resource.js │ │ │ │ ├── http │ │ │ │ │ └── client.js │ │ │ │ └── browser.js │ │ │ └── hal.js │ │ │ ├── doc │ │ │ └── product.html │ │ │ ├── login.html │ │ │ ├── index.html │ │ │ └── browser.html │ │ └── java │ │ └── de │ │ └── otto │ │ └── edison │ │ └── hal │ │ └── example │ │ ├── Server.java │ │ ├── web │ │ ├── RelsController.java │ │ ├── HomeController.java │ │ ├── ProductHalJson.java │ │ ├── ProductsController.java │ │ └── ProductsHalJson.java │ │ └── shop │ │ ├── Product.java │ │ └── ProductSearchService.java └── build.gradle ├── .idea ├── vcs.xml ├── compiler.xml ├── .gitignore ├── misc.xml ├── gradle.xml └── jarRepositories.xml ├── .travis.yml ├── src ├── test │ ├── java │ │ └── de │ │ │ └── otto │ │ │ └── edison │ │ │ └── hal │ │ │ ├── paging │ │ │ ├── PagingRelTest.java │ │ │ ├── ZeroBasedNumberedPagingTest.java │ │ │ ├── OneBasedNumberedPagingTest.java │ │ │ └── SkipLimitPagingTest.java │ │ │ ├── HalRepresentationTest.java │ │ │ ├── HalRepresentationAttributesTest.java │ │ │ ├── CuriesTest.java │ │ │ ├── CuriTemplateTest.java │ │ │ ├── EmbeddedTest.java │ │ │ ├── LinksBuilderTest.java │ │ │ ├── HalRepresentationCuriesTest.java │ │ │ ├── LinkTest.java │ │ │ └── UserGuideExamples.java │ └── resources │ │ └── logback-test.xml └── main │ └── java │ └── de │ └── otto │ └── edison │ └── hal │ ├── paging │ └── PagingRel.java │ ├── traverson │ ├── LinkResolver.java │ └── PageHandler.java │ ├── EmbeddedTypeInfo.java │ ├── LinkPredicates.java │ ├── Curies.java │ └── HalRepresentation.java ├── .github ├── dependabot.yml └── workflows │ └── gradle.yml ├── example-client ├── src │ └── main │ │ └── java │ │ └── de │ │ └── otto │ │ └── edison │ │ └── hal │ │ └── example │ │ ├── Client.java │ │ ├── BookHalJson.java │ │ └── HalShopClient.java └── build.gradle ├── release.sh ├── README.md ├── gradlew.bat ├── gradlew └── CHANGES.md /codecov.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /MAINTAINERS: -------------------------------------------------------------------------------- 1 | Team FT3 2 | -------------------------------------------------------------------------------- /OSSMETADATA: -------------------------------------------------------------------------------- 1 | osslifecycle=active 2 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | sonatypeUsername= 2 | sonatypePassword= -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | out 3 | .gradle 4 | *.iml 5 | *.ipr 6 | *.iws 7 | .shelf 8 | .idea/* -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'edison-hal' 2 | include 'example-springboot' 3 | include 'example-client' 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de/edison-hal/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/vendor/img/ajax-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de/edison-hal/HEAD/example-springboot/src/main/resources/static/vendor/img/ajax-loader.gif -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - openjdk17 4 | install: /bin/true # skip gradle assemble 5 | script: ./gradlew check jacocoTestReport 6 | after_success: 7 | - bash <(curl -s https://codecov.io/bash) 8 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/vendor/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de/edison-hal/HEAD/example-springboot/src/main/resources/static/vendor/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/vendor/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/otto-de/edison-hal/HEAD/example-springboot/src/main/resources/static/vendor/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/documentation.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Documenation = Backbone.View.extend({ 2 | className: 'documentation', 3 | 4 | render: function(url) { 5 | this.$el.html(''); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/paging/PagingRelTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.paging; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.hamcrest.MatcherAssert.assertThat; 6 | import static org.hamcrest.Matchers.is; 7 | 8 | public class PagingRelTest { 9 | 10 | 11 | @Test 12 | public void shouldLowerCaseRel() { 13 | assertThat(PagingRel.LAST.toString(), is("last")); 14 | } 15 | } -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/properties.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Properties = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | _.bindAll(this, 'render'); 5 | }, 6 | 7 | className: 'properties', 8 | 9 | render: function(props) { 10 | this.$el.html(this.template({ properties: props })); 11 | }, 12 | 13 | template: _.template($('#properties-template').html()) 14 | }); 15 | -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/Server.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.annotation.ComponentScan; 6 | 7 | @ComponentScan("de.otto.edison.hal.example") 8 | @SpringBootApplication 9 | public class Server { 10 | public static void main(String[] args) { 11 | SpringApplication.run(Server.class, args); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gradle" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example-client/src/main/java/de/otto/edison/hal/example/Client.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example; 2 | 3 | import java.io.IOException; 4 | 5 | public class Client { 6 | 7 | public static void main(String[] args) { 8 | try (final HalShopClient shopClient = new HalShopClient()) { 9 | shopClient.traverse("", true); 10 | shopClient.traverse("Spring", false); 11 | } catch (final IOException e) { 12 | System.out.println("\n\n\tPlease first start example-springboot so we can get some products from a server"); 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/navigation.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Navigation = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | this.locationBar = new HAL.Views.LocationBar({ vent: this.vent }); 5 | this.requestHeadersView = new HAL.Views.RequestHeaders({ vent: this.vent }); 6 | }, 7 | 8 | className: 'navigation', 9 | 10 | render: function() { 11 | this.$el.empty(); 12 | 13 | this.locationBar.render(); 14 | this.requestHeadersView.render(); 15 | 16 | this.$el.append(this.locationBar.el); 17 | this.$el.append(this.requestHeadersView.el); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /example-client/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | plugins { 8 | id 'java' 9 | id 'idea' 10 | id 'application' 11 | } 12 | 13 | application { 14 | mainClassName = 'de.otto.edison.hal.example.Client' 15 | } 16 | 17 | jar { 18 | archiveBaseName = 'edison-hal-example-client' 19 | } 20 | 21 | java { 22 | targetCompatibility = JavaVersion.VERSION_17 23 | sourceCompatibility = JavaVersion.VERSION_17 24 | } 25 | 26 | repositories { 27 | mavenCentral() 28 | } 29 | 30 | dependencies { 31 | implementation rootProject 32 | implementation("org.apache.httpcomponents:httpclient:4.5.14") 33 | } -------------------------------------------------------------------------------- /example-springboot/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id('java') 3 | id('idea') 4 | id("org.springframework.boot") version "3.4.4" 5 | } 6 | 7 | jar { 8 | archiveBaseName = 'edison-hal-example-springboot' 9 | } 10 | 11 | java { 12 | targetCompatibility = JavaVersion.VERSION_17 13 | sourceCompatibility = JavaVersion.VERSION_17 14 | } 15 | 16 | repositories { 17 | mavenCentral() 18 | } 19 | 20 | dependencies { 21 | var springBootVersion = "3.4.4" 22 | 23 | implementation rootProject 24 | implementation("org.springframework.boot:spring-boot-starter:${springBootVersion}") 25 | implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}") 26 | } -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/web/RelsController.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.PathVariable; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | 7 | /** 8 | * Returns HTML documentation for a single link-relation type. 9 | */ 10 | @Controller 11 | public class RelsController { 12 | 13 | @RequestMapping( 14 | path = "/rels/{rel}", 15 | produces = {"text/html", "*/*"} 16 | ) 17 | public String getRel(@PathVariable String rel) { 18 | return "/doc/" + rel + ".html"; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/explorer.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Explorer = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | var self = this; 4 | this.vent = opts.vent; 5 | this.navigationView = new HAL.Views.Navigation({ vent: this.vent }); 6 | this.resourceView = new HAL.Views.Resource({ vent: this.vent }); 7 | }, 8 | 9 | className: 'explorer span6', 10 | 11 | render: function() { 12 | this.navigationView.render(); 13 | 14 | this.$el.html(this.template()); 15 | 16 | this.$el.append(this.navigationView.el); 17 | this.$el.append(this.resourceView.el); 18 | }, 19 | 20 | template: function() { 21 | return '

Explorer

'; 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/response.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Response = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | 5 | this.headersView = new HAL.Views.ResponseHeaders({ vent: this.vent }); 6 | this.bodyView = new HAL.Views.ResponseBody({ vent: this.vent }); 7 | 8 | _.bindAll(this, 'render'); 9 | 10 | this.vent.bind('response', this.render); 11 | }, 12 | 13 | className: 'response', 14 | 15 | render: function(e) { 16 | this.$el.html(); 17 | 18 | this.headersView.render(e); 19 | this.bodyView.render(e); 20 | 21 | this.$el.append(this.headersView.el); 22 | this.$el.append(this.bodyView.el); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | -------------------------------------------------------------------------------- /example-client/src/main/java/de/otto/edison/hal/example/BookHalJson.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import de.otto.edison.hal.HalRepresentation; 5 | 6 | /** 7 | *

8 | * Client-side HAL representation of a single product / book. 9 | *

10 | *

11 | * Note that this example does not use all properties from the server-side 12 | * {@code de.otto.edison.hal.example.web.ProductHalJson} HalRepresentation! 13 | *

14 | *

15 | * Book instances are only created by the parser, so we do not need to care about links. 16 | *

17 | */ 18 | public class BookHalJson extends HalRepresentation { 19 | 20 | @JsonProperty 21 | public String title; 22 | @JsonProperty 23 | public long retailPrice; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Gradle 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle 3 | 4 | name: Build 5 | 6 | on: 7 | workflow_dispatch: 8 | push: 9 | branches: [ main ] 10 | pull_request: 11 | branches: [ main ] 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up JDK 17 21 | uses: actions/setup-java@v3 22 | with: 23 | distribution: 'temurin' 24 | java-version: 17 25 | cache: gradle 26 | - name: Grant execute permission for gradlew 27 | run: chmod +x gradlew 28 | - name: Build with Gradle 29 | run: ./gradlew check -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/response_headers.js: -------------------------------------------------------------------------------- 1 | HAL.Views.ResponseHeaders = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | }, 5 | 6 | events: { 7 | 'click .follow': 'followLink' 8 | }, 9 | 10 | className: 'response-headers', 11 | 12 | followLink: function(e) { 13 | e.preventDefault(); 14 | var $target = $(e.currentTarget); 15 | var uri = $target.attr('href'); 16 | window.location.hash = uri; 17 | }, 18 | 19 | render: function(e) { 20 | this.$el.html(this.template({ 21 | status: { 22 | code: e.jqxhr.status, 23 | text: e.jqxhr.statusText 24 | }, 25 | headers: HAL.parseHeaders(e.jqxhr.getAllResponseHeaders()) 26 | })); 27 | }, 28 | 29 | template: _.template($('#response-headers-template').html()) 30 | }); 31 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/request_headers.js: -------------------------------------------------------------------------------- 1 | HAL.Views.RequestHeaders = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | var self = this; 4 | this.vent = opts.vent; 5 | 6 | _.bindAll(this, 'updateRequestHeaders'); 7 | 8 | this.vent.bind('app:loaded', function() { 9 | self.updateRequestHeaders(); 10 | }); 11 | }, 12 | 13 | className: 'request-headers', 14 | 15 | events: { 16 | 'blur textarea': 'updateRequestHeaders' 17 | }, 18 | 19 | updateRequestHeaders: function(e) { 20 | var inputText = this.$('textarea').val() || ''; 21 | headers = HAL.parseHeaders(inputText); 22 | HAL.client.updateHeaders(_.defaults(headers, HAL.client.defaultHeaders)) 23 | }, 24 | 25 | render: function() { 26 | this.$el.html(this.template()); 27 | }, 28 | 29 | template: _.template($('#request-headers-template').html()) 30 | }); 31 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/browser.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Browser = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | var self = this; 4 | this.vent = opts.vent; 5 | this.entryPoint = opts.entryPoint; 6 | this.explorerView = new HAL.Views.Explorer({ vent: this.vent }); 7 | this.inspectorView = new HAL.Views.Inspector({ vent: this.vent }); 8 | }, 9 | 10 | className: 'hal-browser row-fluid', 11 | 12 | render: function() { 13 | this.$el.empty(); 14 | 15 | this.inspectorView.render(); 16 | this.explorerView.render(); 17 | 18 | this.$el.html(this.explorerView.el); 19 | this.$el.append(this.inspectorView.el); 20 | 21 | var entryPoint = this.entryPoint; 22 | 23 | $("#entryPointLink").click(function(event) { 24 | event.preventDefault(); 25 | window.location.hash = entryPoint; 26 | }); 27 | return this; 28 | } 29 | }); 30 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/inspector.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Inspector = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | 5 | _.bindAll(this, 'renderDocumentation'); 6 | _.bindAll(this, 'renderResponse'); 7 | 8 | this.vent.bind('show-docs', this.renderDocumentation); 9 | this.vent.bind('response', this.renderResponse); 10 | }, 11 | 12 | className: 'inspector span6', 13 | 14 | render: function() { 15 | this.$el.html(this.template()); 16 | }, 17 | 18 | renderResponse: function(response) { 19 | var responseView = new HAL.Views.Response({ vent: this.vent }); 20 | 21 | this.render(); 22 | responseView.render(response); 23 | 24 | this.$el.append(responseView.el); 25 | }, 26 | 27 | renderDocumentation: function(e) { 28 | var docView = new HAL.Views.Documenation({ vent: this.vent }); 29 | 30 | this.render(); 31 | docView.render(e.url); 32 | 33 | this.$el.append(docView.el); 34 | }, 35 | 36 | template: function() { 37 | return '

Inspector

'; 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/shop/Product.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example.shop; 2 | 3 | import java.util.UUID; 4 | 5 | /** 6 | *

7 | * A product in our example HAL shop. 8 | *

9 | *

10 | * Only a few attributes, just enough to illustrate how to use edison-hal. 11 | *

12 | */ 13 | public final class Product { 14 | 15 | public final String id = UUID.randomUUID().toString(); 16 | public final String title; 17 | public final String description; 18 | public final long retailPrice; 19 | 20 | /** 21 | * Build a new Product. 22 | * 23 | * @param title title / name of the product 24 | * @param description some short description of the product 25 | * @param retailPrice the retail price in cent. 26 | */ 27 | public Product(final String title, 28 | final String description, 29 | final long retailPrice) { 30 | this.title = title; 31 | this.description = description; 32 | this.retailPrice = retailPrice; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/embedded_resources.js: -------------------------------------------------------------------------------- 1 | HAL.Views.EmbeddedResources = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | _.bindAll(this, 'render'); 5 | }, 6 | 7 | className: 'embedded-resources accordion', 8 | 9 | render: function(resources) { 10 | var self = this, 11 | resourceViews = [], 12 | buildView = function(resource) { 13 | return new HAL.Views.EmbeddedResource({ 14 | resource: resource, 15 | vent: self.vent 16 | }); 17 | }; 18 | 19 | _.each(resources, function(prop) { 20 | if ($.isArray(prop)) { 21 | _.each(prop, function(resource) { 22 | resourceViews.push(buildView(resource)); 23 | }); 24 | } else { 25 | resourceViews.push(buildView(prop)); 26 | } 27 | }); 28 | 29 | this.$el.html(this.template()); 30 | 31 | _.each(resourceViews, function(view) { 32 | view.render(); 33 | self.$el.append(view.el); 34 | }); 35 | 36 | 37 | return this; 38 | }, 39 | 40 | template: _.template($('#embedded-resources-template').html()) 41 | }); 42 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/doc/product.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | /rels/product 6 | 7 | 8 |

http://localhost:8080/rels/product

9 |

GET

10 |

Get a single product.

11 |

Request

12 | 20 |

Responses

21 | 47 | 48 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/location_bar.js: -------------------------------------------------------------------------------- 1 | HAL.Views.LocationBar = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | _.bindAll(this, 'render'); 5 | _.bindAll(this, 'onButtonClick'); 6 | this.vent.bind('location-change', this.render); 7 | this.vent.bind('location-change', _.bind(this.showSpinner, this)); 8 | this.vent.bind('response', _.bind(this.hideSpinner, this)); 9 | }, 10 | 11 | events: { 12 | 'submit form': 'onButtonClick' 13 | }, 14 | 15 | className: 'address', 16 | 17 | render: function(e) { 18 | e = e || { url: '' }; 19 | this.$el.html(this.template(e)); 20 | }, 21 | 22 | onButtonClick: function(e) { 23 | e.preventDefault(); 24 | this.vent.trigger('location-go', this.getLocation()); 25 | }, 26 | 27 | getLocation: function() { 28 | return this.$el.find('input').val(); 29 | }, 30 | 31 | showSpinner: function() { 32 | this.$el.find('.ajax-loader').addClass('visible'); 33 | }, 34 | 35 | hideSpinner: function() { 36 | this.$el.find('.ajax-loader').removeClass('visible'); 37 | }, 38 | 39 | template: _.template($('#location-bar-template').html()) 40 | }); 41 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/paging/PagingRel.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.paging; 2 | 3 | import de.otto.edison.hal.Links; 4 | 5 | /** 6 | * Link-relation types used in paged resources. 7 | * 8 | * @see IANA link-relations 9 | */ 10 | public enum PagingRel { 11 | /** Conveys an identifier for the link's context. */ 12 | SELF, 13 | /** An IRI that refers to the furthest preceding resource in a series of resources. */ 14 | FIRST, 15 | /** Indicates that the link's context is a part of a series, and that the previous in the series is the link target. */ 16 | PREV, 17 | /** Indicates that the link's context is a part of a series, and that the next in the series is the link target. */ 18 | NEXT, 19 | /** An IRI that refers to the furthest following resource in a series of resources. */ 20 | LAST; 21 | 22 | /** 23 | * Returns a link-relation type in lower-case format so it is usable in 'rel' attributes of {@link Links} 24 | * 25 | * @return link-relation type conforming to IANA 26 | * @see IANA link-relations 27 | */ 28 | public String toString() { 29 | return name().toLowerCase(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/resource.js: -------------------------------------------------------------------------------- 1 | HAL.Models.Resource = Backbone.Model.extend({ 2 | initialize: function(representation) { 3 | representation = representation || {}; 4 | this.links = representation._links; 5 | this.title = representation.title; 6 | if(representation._embedded !== undefined) { 7 | this.embeddedResources = this.buildEmbeddedResources(representation._embedded); 8 | } 9 | this.set(representation); 10 | this.unset('_embedded', { silent: true }); 11 | this.unset('_links', { silent: true }); 12 | }, 13 | 14 | buildEmbeddedResources: function(embeddedResources) { 15 | var result = {}; 16 | _.each(embeddedResources, function(obj, rel) { 17 | if($.isArray(obj)) { 18 | var arr = []; 19 | _.each(obj, function(resource, i) { 20 | var newResource = new HAL.Models.Resource(resource); 21 | newResource.identifier = rel + '[' + i + ']'; 22 | newResource.embed_rel = rel; 23 | arr.push(newResource); 24 | }); 25 | result[rel] = arr; 26 | } else { 27 | var newResource = new HAL.Models.Resource(obj); 28 | newResource.identifier = rel; 29 | newResource.embed_rel = rel; 30 | result[rel] = newResource; 31 | } 32 | }); 33 | return result; 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/resource.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Resource = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | var self = this; 4 | 5 | this.vent = opts.vent; 6 | 7 | this.vent.bind('response', function(e) { 8 | self.render(new HAL.Models.Resource(e.resource)); 9 | }); 10 | 11 | this.vent.bind('fail-response', function(e) { 12 | try { 13 | resource = JSON.parse(e.jqxhr.responseText); 14 | } catch(err) { 15 | resource = null; 16 | } 17 | self.vent.trigger('response', { resource: resource, jqxhr: e.jqxhr }); 18 | }); 19 | }, 20 | 21 | className: 'resource', 22 | 23 | render: function(resource) { 24 | var linksView = new HAL.Views.Links({ vent: this.vent }), 25 | propertiesView = new HAL.Views.Properties({ vent: this.vent }), 26 | embeddedResourcesView 27 | 28 | propertiesView.render(resource.toJSON()); 29 | linksView.render(resource.links); 30 | 31 | this.$el.empty(); 32 | this.$el.append(propertiesView.el); 33 | this.$el.append(linksView.el); 34 | 35 | if (resource.embeddedResources) { 36 | embeddedResourcesView = new HAL.Views.EmbeddedResources({ vent: this.vent }); 37 | embeddedResourcesView.render(resource.embeddedResources); 38 | this.$el.append(embeddedResourcesView.el); 39 | } 40 | 41 | return this; 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/response_body.js: -------------------------------------------------------------------------------- 1 | HAL.Views.ResponseBody = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | }, 5 | 6 | className: 'response-headers', 7 | 8 | render: function(e) { 9 | this.$el.html(this.template({ 10 | body: this._bodyAsStringFromEvent(e) 11 | })); 12 | }, 13 | 14 | template: _.template($('#response-body-template').html()), 15 | 16 | _bodyAsStringFromEvent: function(e) { 17 | var output = 'n/a'; 18 | if(e.resource !== null) { 19 | output = JSON.stringify(e.resource, null, HAL.jsonIndent); 20 | } else { 21 | // The Ajax request "failed", but there may still be an 22 | // interesting response body (possibly JSON) to show. 23 | var content_type = e.jqxhr.getResponseHeader('content-type'); 24 | var responseText = e.jqxhr.responseText; 25 | if(content_type == null || content_type.indexOf('text/') == 0) { 26 | output = responseText; 27 | } else if(content_type.indexOf('json') != -1) { 28 | // Looks like json... try to parse it. 29 | try { 30 | var obj = JSON.parse(responseText); 31 | output = JSON.stringify(obj, null, HAL.jsonIndent); 32 | } catch (err) { 33 | // JSON parse failed. Just show the raw text. 34 | output = responseText; 35 | } 36 | } 37 | } 38 | return output 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/traverson/LinkResolver.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.traverson; 2 | 3 | import de.otto.edison.hal.Link; 4 | 5 | import java.io.IOException; 6 | 7 | /** 8 | * Functional interface used to resolve a {@link de.otto.edison.hal.Link} and return the {@code application/hal+json} 9 | * resource as a String. 10 | *

11 | * The function will be called by the Traverson, whenever a Link must be followed. The Traverson will 12 | * take care of URI templates (templated links), so the implementation of the function can rely on the 13 | * Link parameter to be not {@link Link#isTemplated() templated}. 14 | *

15 | *

16 | * Typical implementations of the Function will rely on some HTTP client. Especially in this case, 17 | * the function should take care of the link's {@link Link#getType() type} and {@link Link#getProfile()}, 18 | * so the proper HTTP Accept header is used. 19 | *

20 | * @since 2.0.0 21 | */ 22 | @FunctionalInterface 23 | public interface LinkResolver { 24 | 25 | /** 26 | * Resolves an absolute link and returns the linked resource representation as a String. 27 | * 28 | * @param link the link of the resource 29 | * @return String containing the {@code application/hal+json} representation of the resource 30 | * 31 | * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. 32 | */ 33 | String apply(Link link) throws IOException; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/web/HomeController.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example.web; 2 | 3 | import de.otto.edison.hal.HalRepresentation; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RequestMethod; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | import jakarta.servlet.http.HttpServletRequest; 9 | 10 | import static de.otto.edison.hal.Link.linkBuilder; 11 | import static de.otto.edison.hal.Links.linkingTo; 12 | 13 | @RestController 14 | public class HomeController { 15 | 16 | /** 17 | * Entry point for the products REST API. 18 | * 19 | * @param request current request 20 | * @return application/hal+json document containing links to the API. 21 | */ 22 | @RequestMapping( 23 | path = "/api", 24 | method = RequestMethod.GET, 25 | produces = {"application/hal+json", "application/json"} 26 | ) 27 | public HalRepresentation getHomeDocument(final HttpServletRequest request) { 28 | final String homeUrl = request.getRequestURL().toString(); 29 | return new HalRepresentation( 30 | linkingTo() 31 | .self(homeUrl) 32 | .single(linkBuilder("search", "/api/products{?q,embedded}") 33 | .withTitle("Search Products") 34 | .withType("application/hal+json") 35 | .build()) 36 | .build() 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/links.js: -------------------------------------------------------------------------------- 1 | HAL.Views.Links = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | }, 5 | 6 | events: { 7 | 'click .follow': 'followLink', 8 | 'click .non-get': 'showNonSafeRequestDialog', 9 | 'click .query': 'showUriQueryDialog', 10 | 'click .dox': 'showDocs' 11 | }, 12 | 13 | className: 'links', 14 | 15 | followLink: function(e) { 16 | e.preventDefault(); 17 | var $target = $(e.currentTarget); 18 | var uri = $target.attr('href'); 19 | window.location.hash = uri; 20 | }, 21 | 22 | showUriQueryDialog: function(e) { 23 | e.preventDefault(); 24 | 25 | var $target = $(e.currentTarget); 26 | var uri = $target.attr('href'); 27 | 28 | new HAL.Views.QueryUriDialog({ 29 | href: uri 30 | }).render({}); 31 | }, 32 | 33 | showNonSafeRequestDialog: function(e) { 34 | e.preventDefault(); 35 | 36 | var postForm = (HAL.customPostForm !== undefined) ? HAL.customPostForm : HAL.Views.NonSafeRequestDialog; 37 | var d = new postForm({ 38 | href: $(e.currentTarget).attr('href'), 39 | vent: this.vent 40 | }).render({}) 41 | }, 42 | 43 | showDocs: function(e) { 44 | e.preventDefault(); 45 | var $target = $(e.target); 46 | var uri = $target.attr('href') || $target.parent().attr('href'); 47 | this.vent.trigger('show-docs', { url: uri }); 48 | }, 49 | 50 | template: _.template($('#links-template').html()), 51 | 52 | render: function(links) { 53 | this.$el.html(this.template({ links: links })); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/http/client.js: -------------------------------------------------------------------------------- 1 | HAL.Http.Client = function(opts) { 2 | this.vent = opts.vent; 3 | this.defaultHeaders = { 'Accept': 'application/hal+json, application/json, */*; q=0.01' }; 4 | cookie = document.cookie.match('(^|;)\\s*' + 'MyHalBrowserToken' + '\\s*=\\s*([^;]+)'); 5 | cookie ? this.defaultHeaders.Authorization = 'Bearer ' + cookie.pop() : ''; 6 | this.headers = this.defaultHeaders; 7 | }; 8 | 9 | HAL.Http.Client.prototype.get = function(url) { 10 | var self = this; 11 | this.vent.trigger('location-change', { url: url }); 12 | var jqxhr = $.ajax({ 13 | url: url, 14 | dataType: 'json', 15 | xhrFields: { 16 | withCredentials: false 17 | }, 18 | headers: this.headers, 19 | success: function(resource, textStatus, jqXHR) { 20 | self.vent.trigger('response', { 21 | resource: resource, 22 | jqxhr: jqXHR, 23 | headers: jqXHR.getAllResponseHeaders() 24 | }); 25 | } 26 | }).error(function() { 27 | self.vent.trigger('fail-response', { jqxhr: jqxhr }); 28 | }); 29 | }; 30 | 31 | HAL.Http.Client.prototype.request = function(opts) { 32 | var self = this; 33 | opts.dataType = 'json'; 34 | opts.xhrFields = opts.xhrFields || {}; 35 | opts.xhrFields.withCredentials = opts.xhrFields.withCredentials || false; 36 | self.vent.trigger('location-change', { url: opts.url }); 37 | return jqxhr = $.ajax(opts); 38 | }; 39 | 40 | HAL.Http.Client.prototype.updateHeaders = function(headers) { 41 | this.headers = headers; 42 | }; 43 | 44 | HAL.Http.Client.prototype.getHeaders = function() { 45 | return this.headers; 46 | }; 47 | -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/web/ProductHalJson.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example.web; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import de.otto.edison.hal.HalRepresentation; 5 | import de.otto.edison.hal.example.shop.Product; 6 | 7 | import static de.otto.edison.hal.Link.linkBuilder; 8 | import static de.otto.edison.hal.Links.linkingTo; 9 | 10 | /** 11 | *

12 | * HAL representation of a single product. 13 | *

14 | *

15 | * This is an example on how to provide custom properties for a HAL document. 16 | *

17 | */ 18 | class ProductHalJson extends HalRepresentation { 19 | 20 | @JsonProperty 21 | private String title; 22 | @JsonProperty 23 | private String description; 24 | @JsonProperty 25 | private long retailPrice; 26 | 27 | public ProductHalJson(final Product product) { 28 | super( 29 | linkingTo() 30 | .single(linkBuilder("self", "/api/products/" + product.id) 31 | .withType("application/hal+json") 32 | .withTitle(product.title) 33 | .build()) 34 | .single(linkBuilder("collection", "/api/products") 35 | .withTitle("All Products") 36 | .withType("application/hal+json") 37 | .build()) 38 | .build() 39 | ); 40 | 41 | title = product.title; 42 | description = product.description; 43 | retailPrice = product.retailPrice; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/browser.js: -------------------------------------------------------------------------------- 1 | HAL.Browser = Backbone.Router.extend({ 2 | initialize: function(opts) { 3 | opts = opts || {}; 4 | 5 | var vent = _.extend({}, Backbone.Events), 6 | $container = opts.container || $('#browser'); 7 | 8 | this.entryPoint = opts.entryPoint || '/'; 9 | 10 | // TODO: don't hang currentDoc off namespace 11 | vent.bind('response', function(e) { 12 | window.HAL.currentDocument = e.resource || {}; 13 | }); 14 | 15 | vent.bind('location-go', _.bind(this.loadUrl, this)); 16 | 17 | HAL.client = new HAL.Http.Client({ vent: vent }); 18 | 19 | var browser = new HAL.Views.Browser({ vent: vent, entryPoint: this.entryPoint }); 20 | browser.render() 21 | 22 | $container.html(browser.el); 23 | vent.trigger('app:loaded'); 24 | 25 | if (window.location.hash === '') { 26 | window.location.hash = this.entryPoint; 27 | } 28 | 29 | if(location.hash.slice(1,9) === 'NON-GET:') { 30 | new HAL.Views.NonSafeRequestDialog({ 31 | href: location.hash.slice(9), 32 | vent: vent 33 | }).render({}); 34 | } 35 | }, 36 | 37 | routes: { 38 | '*url': 'resourceRoute' 39 | }, 40 | 41 | loadUrl: function(url) { 42 | if (this.getHash() === url) { 43 | HAL.client.get(url); 44 | } else { 45 | window.location.hash = url; 46 | } 47 | }, 48 | 49 | getHash: function() { 50 | return window.location.hash.slice(1); 51 | }, 52 | 53 | resourceRoute: function() { 54 | url = location.hash.slice(1); 55 | console.log('target url changed to: ' + url); 56 | if (url.slice(0,8) !== 'NON-GET:') { 57 | HAL.client.get(url); 58 | } 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/non_safe_request_dialog.js: -------------------------------------------------------------------------------- 1 | HAL.Views.NonSafeRequestDialog = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.href = opts.href; 4 | this.vent = opts.vent; 5 | this.uriTemplate = uritemplate(this.href); 6 | _.bindAll(this, 'submitQuery'); 7 | }, 8 | 9 | events: { 10 | 'submit form': 'submitQuery' 11 | }, 12 | 13 | className: 'modal fade', 14 | 15 | submitQuery: function(e) { 16 | e.preventDefault(); 17 | 18 | var self = this, 19 | opts = { 20 | url: this.$('.url').val(), 21 | headers: HAL.parseHeaders(this.$('.headers').val()), 22 | method: this.$('.method').val(), 23 | data: this.$('.body').val() 24 | }; 25 | 26 | var request = HAL.client.request(opts); 27 | request.done(function(response) { 28 | self.vent.trigger('response', { resource: response, jqxhr: jqxhr }); 29 | }).fail(function(response) { 30 | self.vent.trigger('fail-response', { jqxhr: jqxhr }); 31 | }).always(function() { 32 | self.vent.trigger('response-headers', { jqxhr: jqxhr }); 33 | window.location.hash = 'NON-GET:' + opts.url; 34 | }); 35 | 36 | this.$el.modal('hide'); 37 | }, 38 | 39 | render: function(opts) { 40 | var headers = HAL.client.getHeaders(), 41 | headersString = ''; 42 | 43 | _.each(headers, function(value, name) { 44 | headersString += name + ': ' + value + '\n'; 45 | }); 46 | 47 | this.$el.html(this.template({ href: this.href, user_defined_headers: headersString })); 48 | this.$el.modal(); 49 | return this; 50 | }, 51 | 52 | template: _.template($('#non-safe-request-template').html()) 53 | }); 54 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/traverson/PageHandler.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.traverson; 2 | 3 | import com.fasterxml.jackson.core.JsonParseException; 4 | import com.fasterxml.jackson.databind.JsonMappingException; 5 | import de.otto.edison.hal.EmbeddedTypeInfo; 6 | 7 | import java.io.IOException; 8 | 9 | /** 10 | * Functional interface used to traverse pages of linked or embedded resources. 11 | *

12 | * The different {@link Traverson#paginateAs(String, Class, EmbeddedTypeInfo, PageHandler) pagination} methods 13 | * of the {@link Traverson} make use of PageHandlers to traverse a single page. 14 | *

15 | *

16 |  *     Traverson.traverson(this::fetchJson)
17 |  *          .startWith("http://example.com/example/collection")
18 |  *          .paginateNext( (Traverson pageTraverson) -> {
19 |  *              pageTraverson
20 |  *                      .follow("item")
21 |  *                      .streamAs(OtherExtendedHalRepresentation.class)
22 |  *                      .forEach(x -> values.add(x.someOtherProperty));
23 |  *              return true;
24 |  *          });
25 |  * 
26 | * @since 2.0.0 27 | */ 28 | @FunctionalInterface 29 | public interface PageHandler { 30 | 31 | /** 32 | * Processes a single page and decides whether or not to proceed to the following page. 33 | * 34 | * @param traverson the Traverson used to process the current page. 35 | * @return true if traversion should continue, false if it should be aborted. 36 | * 37 | * @throws IOException if a low-level I/O problem (unexpected end-of-input, network error) occurs. 38 | * @throws JsonParseException if the json document can not be parsed by Jackson's ObjectMapper 39 | * @throws JsonMappingException if the input JSON structure can not be mapped to the specified HalRepresentation type 40 | */ 41 | boolean apply(Traverson traverson) throws IOException; 42 | 43 | } 44 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/embedded_resource.js: -------------------------------------------------------------------------------- 1 | HAL.Views.EmbeddedResource = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.vent = opts.vent; 4 | this.resource = opts.resource; 5 | 6 | this.propertiesView = new HAL.Views.Properties({}); 7 | this.linksView = new HAL.Views.Links({ 8 | vent: this.vent 9 | }); 10 | 11 | _.bindAll(this, 'onToggleClick'); 12 | _.bindAll(this, 'onDoxClick'); 13 | }, 14 | 15 | events: { 16 | 'click a.accordion-toggle': 'onToggleClick', 17 | 'click span.dox': 'onDoxClick' 18 | }, 19 | 20 | className: 'embedded-resource accordion-group', 21 | 22 | onToggleClick: function(e) { 23 | e.preventDefault(); 24 | this.$accordionBody.collapse('toggle'); 25 | return false; 26 | }, 27 | 28 | onDoxClick: function(e) { 29 | e.preventDefault(); 30 | this.vent.trigger('show-docs', { 31 | url: $(e.currentTarget).data('href') 32 | }); 33 | return false; 34 | }, 35 | 36 | render: function() { 37 | this.$el.empty(); 38 | 39 | this.propertiesView.render(this.resource.toJSON()); 40 | this.linksView.render(this.resource.links); 41 | 42 | this.$el.append(this.template({ 43 | resource: this.resource 44 | })); 45 | 46 | var $inner = $('
'); 47 | $inner.append(this.propertiesView.el); 48 | $inner.append(this.linksView.el); 49 | 50 | if (this.resource.embeddedResources) { 51 | var embeddedResourcesView = new HAL.Views.EmbeddedResources({ vent: this.vent }); 52 | embeddedResourcesView.render(this.resource.embeddedResources); 53 | $inner.append(embeddedResourcesView.el); 54 | } 55 | 56 | this.$accordionBody = $('
'); 57 | this.$accordionBody.append($inner) 58 | 59 | this.$el.append(this.$accordionBody); 60 | }, 61 | 62 | template: _.template($('#embedded-resource-template').html()) 63 | }); 64 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/HalRepresentationTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.junit.Test; 7 | 8 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; 9 | import static de.otto.edison.hal.Embedded.emptyEmbedded; 10 | import static de.otto.edison.hal.Links.linkingTo; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.Matchers.is; 13 | 14 | public class HalRepresentationTest { 15 | 16 | @Test 17 | public void shouldRenderNullAttributes() throws JsonProcessingException { 18 | // given 19 | final HalRepresentation representation = new HalRepresentation( 20 | linkingTo().self("http://example.org/test/bar").build(), 21 | emptyEmbedded()) 22 | { 23 | public String someNullAttr=null; 24 | }; 25 | // when 26 | final String json = new ObjectMapper().writeValueAsString(representation); 27 | // then 28 | assertThat(json, is( 29 | "{" + 30 | "\"someNullAttr\":null," + 31 | "\"_links\":{\"self\":{\"href\":\"http://example.org/test/bar\"}}" + 32 | "}")); 33 | } 34 | 35 | @Test 36 | public void shouldSkipAnnotatedNullAttributes() throws JsonProcessingException { 37 | // given 38 | final HalRepresentation representation = new HalRepresentation( 39 | linkingTo().self("http://example.org/test/bar").build(), 40 | emptyEmbedded()) 41 | { 42 | @JsonInclude(NON_NULL) 43 | public String someNullAttr=null; 44 | }; 45 | // when 46 | final String json = new ObjectMapper().writeValueAsString(representation); 47 | // then 48 | assertThat(json, is( 49 | "{" + 50 | "\"_links\":{\"self\":{\"href\":\"http://example.org/test/bar\"}}" + 51 | "}")); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/web/ProductsController.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example.web; 2 | 3 | import de.otto.edison.hal.HalRepresentation; 4 | import de.otto.edison.hal.example.shop.Product; 5 | import de.otto.edison.hal.example.shop.ProductSearchService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.web.bind.annotation.*; 8 | 9 | import jakarta.servlet.http.HttpServletResponse; 10 | import java.io.IOException; 11 | import java.util.Optional; 12 | 13 | import static java.util.Optional.ofNullable; 14 | import static jakarta.servlet.http.HttpServletResponse.SC_NOT_FOUND; 15 | 16 | /** 17 | * REST controller for products. 18 | */ 19 | @RestController 20 | public class ProductsController { 21 | 22 | @Autowired 23 | private ProductSearchService productSearch; 24 | 25 | /** 26 | * @return application/hal+json document containing links to the API. 27 | */ 28 | @RequestMapping( 29 | path = "/api/products", 30 | method = RequestMethod.GET, 31 | produces = {"application/hal+json", "application/json"} 32 | ) 33 | public HalRepresentation getProducts(@RequestParam(defaultValue = "false") final boolean embedded, 34 | @RequestParam(required = false) final String q) { 35 | return new ProductsHalJson(productSearch.searchFor(ofNullable(q)), embedded); 36 | } 37 | 38 | /** 39 | * @return application/hal+json document containing links to the API. 40 | */ 41 | @RequestMapping( 42 | path = "/api/products/{productId}", 43 | method = RequestMethod.GET, 44 | produces = {"application/hal+json", "application/json"} 45 | ) 46 | public HalRepresentation getProduct(@PathVariable final String productId, 47 | final HttpServletResponse response) throws IOException { 48 | Optional product = productSearch.findBy(productId); 49 | if (product.isPresent()) { 50 | return new ProductHalJson(product.get()); 51 | } else { 52 | response.sendError(SC_NOT_FOUND, "Product " + productId + " not found"); 53 | return null; 54 | } 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | SCRIPT_DIR=$(dirname $0) 5 | 6 | USER_JRELEASER_PROPERTIES=~/.jreleaser/config.toml 7 | 8 | check_configured() { 9 | grep -q $1 ${USER_JRELEASER_PROPERTIES} || ( echo "$1 not configured in ${USER_JRELEASER_PROPERTIES}. $2" && exit 1 ) 10 | } 11 | 12 | check_configuration() { 13 | if [ ! -f ${USER_JRELEASER_PROPERTIES} ]; then 14 | echo "${USER_JRELEASER_PROPERTIES} does not exist. We use JReleaser to release packages. Pleaser create the file and add the following variables:" 15 | echo "JRELEASER_GPG_PASSPHRASE=\"your_gpg_passphrase\"" 16 | echo "JRELEASER_GPG_PUBLIC_KEY=\"your_gpg_public_key\"" 17 | echo "JRELEASER_GPG_SECRET_KEY=\"your_gpg_secret_key\"" 18 | echo "JRELEASER_MAVENCENTRAL_USERNAME=\"your_maven_central_username\"" 19 | echo "JRELEASER_MAVENCENTRAL_PASSWORD=\"your_maven_central_password\"" 20 | echo "JRELEASER_GITHUB_TOKEN=\"your_github_token\"" 21 | exit 1 22 | fi 23 | 24 | check_configured "JRELEASER_GPG_PASSPHRASE" "This is the GPG passphrase for your GPG key" 25 | check_configured "JRELEASER_GPG_PUBLIC_KEY" "This is the GPG public key to sign the release. Use 'gpg --export ***KEYID*** | base64' to export the public key" 26 | check_configured "JRELEASER_GPG_SECRET_KEY" "This is the GPG secret key to sign the release. Use 'gpg --export-secret-keys ***KEYID*** | base64' to export the private key" 27 | check_configured "JRELEASER_MAVENCENTRAL_USERNAME" "This is the Maven Central username to release packages" 28 | check_configured "JRELEASER_MAVENCENTRAL_PASSWORD" "This is the Maven Central password to release packages" 29 | check_configured "JRELEASER_GITHUB_TOKEN" "This it the Github Token to release packages and release information to GitHub" 30 | # for GPG version >= 2.1: gpg --export-secret-keys >~/.gnupg/secring.gpg 31 | # gpg --send-keys --keyserver keys.openpgp.org yourKeyId 32 | } 33 | 34 | check_configuration 35 | 36 | set +e 37 | grep '_version = ".*-SNAPSHOT"' "$SCRIPT_DIR/build.gradle" 38 | SNAPSHOT=$? 39 | set -e 40 | 41 | if [[ $SNAPSHOT == 1 ]]; then 42 | echo "INFO: This is not a SNAPSHOT, I'll release to Maven Central during upload." 43 | else 44 | echo "INFO: This is a SNAPSHOT release. Packages will be released to GitHub packages only." 45 | fi 46 | 47 | "${SCRIPT_DIR}"/gradlew clean jreleaserConfig check 48 | "${SCRIPT_DIR}"/gradlew build publish 49 | "${SCRIPT_DIR}"/gradlew jreleaserFullRelease 50 | 51 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal/views/query_uri_dialog.js: -------------------------------------------------------------------------------- 1 | HAL.Views.QueryUriDialog = Backbone.View.extend({ 2 | initialize: function(opts) { 3 | this.href = opts.href; 4 | this.uriTemplate = uritemplate(this.href); 5 | _.bindAll(this, 'submitQuery'); 6 | _.bindAll(this, 'renderPreview'); 7 | }, 8 | 9 | className: 'modal fade', 10 | 11 | events: { 12 | 'submit form': 'submitQuery', 13 | 'keyup textarea': 'renderPreview', 14 | 'change textarea': 'renderPreview' 15 | }, 16 | 17 | submitQuery: function(e) { 18 | e.preventDefault(); 19 | var input; 20 | try { 21 | input = JSON.parse(this.$('textarea').val()); 22 | } catch(err) { 23 | input = {}; 24 | } 25 | this.$el.modal('hide'); 26 | window.location.hash = this.uriTemplate.expand(this.cleanInput(input)); 27 | }, 28 | 29 | renderPreview: function(e) { 30 | var input, result; 31 | try { 32 | input = JSON.parse($(e.target).val()); 33 | result = this.uriTemplate.expand(this.cleanInput(input)); 34 | } catch (err) { 35 | result = 'Invalid json input'; 36 | } 37 | this.$('.preview').text(result); 38 | }, 39 | 40 | extractExpressionNames: function (template) { 41 | var names = []; 42 | for (var i=0; i products, final boolean embedded) { 27 | super( 28 | linkingTo() 29 | .self(fromCurrentRequestUri().toUriString()) 30 | .curi("ex", REL_EXAMPLE_TEMPLATE) 31 | .single(linkBuilder(REL_SEARCH, "/api/products{?q,embedded}") 32 | .withTitle("Search Products") 33 | .withType(APPLICATION_HAL_JSON) 34 | .build()) 35 | .array(products 36 | .stream() 37 | .map(b -> linkBuilder(REL_EXAMPLE_PRODUCT, "/api/products/" + b.id) 38 | .withTitle(b.title) 39 | .withType("application/hal+json") 40 | .build()) 41 | .collect(toList())) 42 | .build(), 43 | embedded ? withEmbedded(products) : emptyEmbedded() 44 | ); 45 | } 46 | 47 | private static Embedded withEmbedded(final List products) { 48 | return embeddedBuilder() 49 | .with(REL_EXAMPLE_PRODUCT, products 50 | .stream() 51 | .map(ProductHalJson::new) 52 | .collect(toList())) 53 | .build(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/js/hal.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var urlRegex = /(http|https):\/\/(\w+:{0,1}\w*@)?(\S+)(:[0-9]+)?(\/|\/([\w#!:.?+=&%@!\-\/]))?/; 3 | 4 | var HAL = { 5 | Models: {}, 6 | Views: {}, 7 | Http: {}, 8 | currentDocument: {}, 9 | jsonIndent: 2, 10 | isUrl: function(str) { 11 | return str.match(urlRegex) || HAL.isCurie(str); 12 | }, 13 | isCurie: function(string) { 14 | var isCurie = false; 15 | var curieParts = string.split(':'); 16 | var curies = HAL.currentDocument._links.curies; 17 | 18 | if(curieParts.length > 1 && curies) { 19 | 20 | for (var i=0; i 1) { 72 | var name = parts.shift().trim(); 73 | var value = parts.join(':').trim(); 74 | headers[name] = value; 75 | } 76 | }); 77 | return headers; 78 | }, 79 | customPostForm: undefined 80 | }; 81 | 82 | window.HAL = HAL; 83 | })(); 84 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sign in - HAL Browser 6 | 7 | 8 | 9 | 38 | 39 | 40 | 63 | 64 | 65 |
66 | 74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/EmbeddedTypeInfo.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import java.util.List; 4 | 5 | import static java.util.Arrays.asList; 6 | import static java.util.Collections.emptyList; 7 | 8 | /** 9 | * Type information for embedded items. This is required if more complex embedded items should be parsed 10 | * into classes derived from HalRepresentation. 11 | * 12 | * @since 0.1.0 13 | */ 14 | public class EmbeddedTypeInfo { 15 | 16 | private final String rel; 17 | 18 | private final Class type; 19 | 20 | private final List nestedTypeInfo; 21 | 22 | /** Creates a new EmbeddedTypeInfo by link-relation type and Java class of embedded items. */ 23 | private EmbeddedTypeInfo(final String rel, 24 | final Class type, 25 | final List nestedTypeInfo) { 26 | this.rel = rel; 27 | this.type = type; 28 | this.nestedTypeInfo = nestedTypeInfo; 29 | } 30 | 31 | public static EmbeddedTypeInfo withEmbedded(final String rel, 32 | final Class embeddedType, 33 | final EmbeddedTypeInfo... nestedTypeInfo) { 34 | if (nestedTypeInfo == null || nestedTypeInfo.length == 0) { 35 | return new EmbeddedTypeInfo(rel, embeddedType, emptyList()); 36 | } else { 37 | return new EmbeddedTypeInfo(rel, embeddedType, asList(nestedTypeInfo)); 38 | } 39 | } 40 | 41 | public static EmbeddedTypeInfo withEmbedded(final String rel, 42 | final Class embeddedType, 43 | final List nestedTypeInfo) { 44 | return new EmbeddedTypeInfo(rel, embeddedType, nestedTypeInfo); 45 | } 46 | 47 | /** 48 | * @return The link-relation type used to identify items of the embedded type. 49 | */ 50 | public String getRel() { 51 | return rel; 52 | } 53 | 54 | /** 55 | * @return The Java class used to deserialize the embedded items for the link-relation type 56 | */ 57 | public Class getType() { 58 | return type; 59 | } 60 | 61 | public List getNestedTypeInfo() { 62 | return nestedTypeInfo != null ? nestedTypeInfo : emptyList(); 63 | } 64 | 65 | @Override 66 | public String toString() { 67 | return "EmbeddedTypeInfo{" + 68 | "rel='" + rel + '\'' + 69 | ", type=" + type.getSimpleName() + 70 | ", nestedTypeInfo=" + nestedTypeInfo + 71 | '}'; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Edison HAL 2 | 3 | Library to produce and consume [application/hal+json](https://tools.ietf.org/html/draft-kelly-json-hal-08) 4 | representations of REST resources using Jackson. 5 | 6 | [![Build Status](https://travis-ci.org/otto-de/edison-hal.svg)](https://travis-ci.org/otto-de/edison-hal) 7 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/de.otto.edison/edison-hal/badge.svg)](https://maven-badges.herokuapp.com/maven-central/de.otto.edison/edison-hal) 8 | [![Javadoc](https://www.javadoc.io/badge/de.otto.edison/edison-hal.svg)](https://www.javadoc.io/doc/de.otto.edison/edison-hal) 9 | [![codecov](https://codecov.io/gh/otto-de/edison-hal/branch/master/graph/badge.svg)](https://codecov.io/gh/otto-de/edison-hal) 10 | ![OSS Lifecycle](https://img.shields.io/osslifecycle?file_url=https%3A%2F%2Fraw.githubusercontent.com%2Fotto-de%2Fedison-hal%2Fmain%2FOSSMETADATA) 11 | 12 | 13 | ## 1. Documentation 14 | * [JavaDoc](https://www.javadoc.io/doc/de.otto.edison/edison-hal) 15 | * [User Guide](./USERGUIDE.md) explains how to use edison-hal in your projects. 16 | * [Change Log](./CHANGES.md) for latest changes. 17 | 18 | ## 2. About 19 | 20 | At otto.de, microservices should only communicate via REST APIs with other 21 | microservices. HAL is a nice format to implement the HATEOAS part 22 | of REST. Edison-HAL is a library to make it easy to produce 23 | and consume HAL representations for your REST APIs. 24 | 25 | Currently, there are only a couple of libraries supporting HAL and even 26 | less that support the full media type including all link properties, 27 | curies (compact URIs) and embedded resources. 28 | 29 | Spring HATEOAS, for example, is lacking many link properties, such as 30 | title, name, type and others. 31 | 32 | ## 3. Features 33 | 34 | Creating HAL representations: 35 | * Links with all specified attributes like rel, href, profile, type, name, title, etc. pp. 36 | * Embedded resources, even deeply nested 37 | * Curies in links and embedded resources 38 | * Generation of HAL representations using Jackson annotations 39 | 40 | Parsing HAL representations: 41 | * Mapping application/hal+json to Java classes using Jackson. This also works for deeply nested embedded items. 42 | * Simple domain model to access links, embedded resources etc. 43 | * Curies are automatically resolved: Given a curi with name=ex and href=http://example.com/rels/{rel}, links 44 | and embedded items can be access either in curied format `ex:foo`, or expanded format `http://exampl.com/rels/foo` 45 | 46 | Traversion of HAL representations: 47 | * Simple client-side navigation through linked and embedded REST resources using 48 | Traverson API 49 | * Embedded resources are transparantly used, if present. 50 | * Curies are resolved transparantly, too. Clients of the Traverson API do not need to 51 | know anything about curies or embedded resources. 52 | -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Edison HAL Example 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 35 | 36 |
37 | 38 | 39 |
40 |

41 | HAL is a simple format that gives a consistent and easy way to hyperlink between resources in your API. 42 |

43 |

44 | Adopting HAL will make your API explorable, and its documentation easily discoverable from within the API 45 | itself. In short, it will make your API easier to work with and therefore more attractive to client 46 | developers. 47 |

48 |

49 | Edison HAL is a small library that makes it easy, to generate or consume HAL documents. 50 |

51 |

52 | Open the HAL Browser» 53 | Learn more about Edison HAL» 54 | Learn more about HAL » 55 |

56 |
57 | 58 |
59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/LinkPredicates.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import java.util.function.Predicate; 4 | 5 | /** 6 | * Predicates used to select links matching some criteria. 7 | * 8 | * @since 1.0.0 9 | */ 10 | public final class LinkPredicates { 11 | 12 | private LinkPredicates() {} 13 | 14 | /** 15 | * Returns a Predicate that is matching every link. 16 | * 17 | * @return Predicate used to select links 18 | */ 19 | public static Predicate always() { 20 | return link -> true; 21 | } 22 | 23 | /** 24 | * Returns a Predicate that is matching links having the specified type {@link Link#getType() type} 25 | * 26 | * @param type the expected media type of the link 27 | * @return Predicate used to select links 28 | */ 29 | public static Predicate havingType(final String type) { 30 | return link -> type.equals(link.getType()); 31 | } 32 | 33 | /** 34 | * Returns a Predicate that is matching links having the specified type {@link Link#getType() type}, or no type 35 | * at all. 36 | * 37 | * @param type the expected media type of the link 38 | * @return Predicate used to select links 39 | */ 40 | public static Predicate optionallyHavingType(final String type) { 41 | return havingType(type).or(havingType("")); 42 | } 43 | 44 | /** 45 | * Returns a Predicate that is matching links having the specified profile {@link Link#getProfile() profile} 46 | * 47 | * @param profile the expected profile of the link 48 | * @return Predicate used to select links 49 | */ 50 | public static Predicate havingProfile(final String profile) { 51 | return link -> profile.equals(link.getProfile()); 52 | } 53 | 54 | /** 55 | * Returns a Predicate that is matching links having the specified profile {@link Link#getProfile() profile}, or 56 | * no profile at all. 57 | * 58 | * @param profile the expected profile of the link 59 | * @return Predicate used to select links 60 | */ 61 | public static Predicate optionallyHavingProfile(final String profile) { 62 | return havingProfile(profile).or(havingProfile("")); 63 | } 64 | 65 | /** 66 | * Returns a Predicate that is matching links having the specified name {@link Link#getName() name} 67 | * 68 | * @param name the expected name of the link 69 | * @return Predicate used to select links 70 | */ 71 | public static Predicate havingName(final String name) { 72 | return link -> name.equals(link.getName()); 73 | } 74 | 75 | /** 76 | * Returns a Predicate that is matching links having the specified name {@link Link#getName() name}, or 77 | * no name at all. 78 | * 79 | * @param name the expected name of the link 80 | * @return Predicate used to select links 81 | */ 82 | public static Predicate optionallyHavingName(final String name) { 83 | return havingName(name).or(havingName("")); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/HalRepresentationAttributesTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.junit.Test; 6 | 7 | import java.io.IOException; 8 | import java.util.List; 9 | 10 | import static de.otto.edison.hal.HalParser.parse; 11 | import static org.hamcrest.MatcherAssert.assertThat; 12 | import static org.hamcrest.Matchers.containsInAnyOrder; 13 | import static org.hamcrest.Matchers.is; 14 | 15 | public class HalRepresentationAttributesTest { 16 | 17 | static class NestedHalRepresentation extends HalRepresentation { 18 | @JsonProperty 19 | public List nested; 20 | } 21 | 22 | @Test 23 | public void shouldParseExtraAttributes() throws IOException { 24 | // given 25 | final String json = "{" + 26 | "\"foo\":\"Hello World\"," + 27 | "\"bar\":[\"Hello\", \"World\"]" + 28 | "}"; 29 | // when 30 | HalRepresentation resource = parse(json).as(HalRepresentation.class); 31 | // then 32 | assertThat(resource.getAttributes().keySet(), containsInAnyOrder("foo", "bar")); 33 | assertThat(resource.getAttribute("foo").asText(), is("Hello World")); 34 | assertThat(resource.getAttribute("bar").at("/0").asText(), is("Hello")); 35 | assertThat(resource.getAttribute("bar").at("/1").asText(), is("World")); 36 | } 37 | 38 | @Test 39 | public void shouldGenerateExtraAttributes() throws IOException { 40 | // given 41 | final String json = "{" + 42 | "\"foo\":\"Hello World\"," + 43 | "\"foobar\":{\"foo\":\"bar\"}," + 44 | "\"bar\":[\"Hello\",\"World\"]" + 45 | "}"; 46 | // when 47 | HalRepresentation resource = parse(json).as(HalRepresentation.class); 48 | final String generatedJson = new ObjectMapper().writeValueAsString(resource); 49 | // then 50 | assertThat(generatedJson, is(json)); 51 | } 52 | 53 | @Test 54 | public void shouldParseAttributesInNestedResourceObjects() throws IOException { 55 | // given 56 | final String json = "{\n" + 57 | " \"nested\" : [\n" + 58 | " {\n" + 59 | " \"name\" : \"Some issue\"\n" + 60 | " }\n" + 61 | " ]\n" + 62 | "}"; 63 | // when 64 | NestedHalRepresentation resource = parse(json).as(NestedHalRepresentation.class); 65 | // then 66 | assertThat(resource.nested.get(0).getAttribute("name").textValue(), is("Some issue")); 67 | } 68 | 69 | @Test 70 | public void shouldGenerateAttributesInNestedResourceObjects() throws IOException { 71 | // given 72 | final String json = "{\n" + 73 | " \"nested\" : [\n" + 74 | " {\n" + 75 | " \"name\" : \"issue\",\n" + 76 | " \"description\" : \"description\"\n" + 77 | " }\n" + 78 | " ]\n" + 79 | "}"; 80 | // when 81 | NestedHalRepresentation resource = parse(json).as(NestedHalRepresentation.class); 82 | final String generatedJson = new ObjectMapper().writeValueAsString(resource); 83 | // then 84 | assertThat(generatedJson, is(json.replaceAll("\\s", ""))); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /example-client/src/main/java/de/otto/edison/hal/example/HalShopClient.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example; 2 | 3 | import de.otto.edison.hal.Link; 4 | import org.apache.http.HttpEntity; 5 | import org.apache.http.client.methods.HttpGet; 6 | import org.apache.http.impl.client.CloseableHttpClient; 7 | import org.apache.http.util.EntityUtils; 8 | 9 | import java.io.IOException; 10 | 11 | import static de.otto.edison.hal.traverson.Traverson.traverson; 12 | import static de.otto.edison.hal.traverson.Traverson.withVars; 13 | import static java.lang.String.format; 14 | import static org.apache.http.impl.client.HttpClients.createDefault; 15 | 16 | /** 17 | *

18 | * A client for the API of example-springboot. 19 | *

20 | *

21 | * The client is using Apache HttpClient to access the /api endpoint of the example server, in order 22 | * to retrieve the entry-point of the products collection resource. It is then following the links to 23 | * get the product information. 24 | *

25 | */ 26 | public class HalShopClient implements AutoCloseable { 27 | 28 | private static final String HOME_URI = "http://localhost:8080/api"; 29 | private static final String REL_SEARCH = "search"; 30 | private static final String REL_PRODUCT = "http://localhost:8080/rels/product"; 31 | 32 | private final CloseableHttpClient httpclient; 33 | 34 | /** 35 | * Create a HalShopClient with a default HttpClient. 36 | */ 37 | public HalShopClient() { 38 | httpclient = createDefault(); 39 | } 40 | 41 | /** 42 | *

43 | * Traverses the API and searches for all products matching the query term. 44 | *

45 | * 46 | * @param query some query term 47 | * @param embeddedProducts query for embedded products 48 | * @see Handy-URI-Templates 49 | */ 50 | public void traverse(final String query, final boolean embeddedProducts) throws IOException { 51 | System.out.println("\n\n"); 52 | System.out.println("---------- Traverson Example ----------"); 53 | traverson(this::getHalJson) 54 | .startWith(HOME_URI) 55 | .follow(REL_SEARCH, withVars("q", query, "embedded", embeddedProducts)) 56 | .follow(REL_PRODUCT) 57 | .streamAs(BookHalJson.class) 58 | .forEach(product->{ 59 | System.out.println("| * " + product.title + ": " + format("%.2f€", product.retailPrice/100.0)); 60 | }); 61 | System.out.println("----------------------------------------"); 62 | } 63 | 64 | /** 65 | * Returns the REST resource identified by {@code uri} as a JSON string. 66 | * 67 | * @param link the non-templated Link of the resource 68 | * @return json 69 | */ 70 | private String getHalJson(final Link link) throws IOException { 71 | try { 72 | final HttpGet httpget = new HttpGet(link.getHref()); 73 | if (link.getType().isEmpty()) { 74 | httpget.addHeader("Accept", "application/hal+json"); 75 | } else { 76 | httpget.addHeader("Accept", link.getType()); 77 | } 78 | 79 | System.out.println("| ------------- Request --------------"); 80 | System.out.println("| " + httpget.getRequestLine()); 81 | 82 | final HttpEntity entity = httpclient.execute(httpget).getEntity(); 83 | final String json = EntityUtils.toString(entity); 84 | System.out.println("| ------------- Response -------------"); 85 | System.out.println("| " + json); 86 | return json; 87 | } catch (final IOException e) { 88 | System.out.println("\nPlease start example-springboot Server before you are running the Client.\n"); 89 | throw e; 90 | } 91 | } 92 | 93 | /** 94 | * Closes the Apache HttpClient. 95 | * 96 | * @throws IOException 97 | */ 98 | @Override 99 | public void close() throws IOException { 100 | httpclient.close(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /example-springboot/src/main/java/de/otto/edison/hal/example/shop/ProductSearchService.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.example.shop; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Optional; 8 | import java.util.function.Predicate; 9 | 10 | import static java.util.stream.Collectors.toList; 11 | 12 | /** 13 | * 14 | * A service used to search for products. 15 | * 16 | */ 17 | @Service 18 | public class ProductSearchService { 19 | 20 | /** 21 | * Some random list of products, consisting of REST books. 22 | */ 23 | private final List products = new ArrayList() {{ 24 | add(new Product( 25 | "REST in Practice: Hypermedia and Systems Architecture", 26 | "Why don't typical enterprise projects go as smoothly as projects you develop for the Web? Does the REST architectural style really present a viable alternative for building distributed systems and enterprise-class applications?\n" + 27 | "\n" + 28 | "In this insightful book, three SOA experts provide a down-to-earth explanation of REST and demonstrate how you can develop simple and elegant distributed hypermedia systems by applying the Web's guiding principles to common enterprise computing problems.", 29 | 2795)); 30 | add(new Product( 31 | "RESTful Web APIs", 32 | "The popularity of REST in recent years has led to tremendous growth in almost-RESTful APIs that don’t include many of the architecture’s benefits. With this practical guide, you’ll learn what it takes to design usable REST APIs that evolve over time. By focusing on solutions that cross a variety of domains, this book shows you how to create powerful and secure applications, using the tools designed for the world’s most successful distributed computing system: the World Wide Web.", 33 | 2995)); 34 | add(new Product( 35 | "Spring REST", 36 | "Spring REST is a practical guide for designing and developing RESTful APIs using the Spring Framework. This book walks you through the process of designing and building a REST application while taking a deep dive into design principles and best practices for versioning, security, documentation, error handling, paging, and sorting.", 37 | 2651)); 38 | add(new Product( 39 | "RESTful Web API Design with Node.js", 40 | "Create a fully featured RESTful API solution from scratch.\n" + 41 | "Learn how to leverage Node.JS, Express, MongoDB and NoSQL datastores to give an extra edge to your REST API design.\n" + 42 | "Use this practical guide to integrate MongoDB in your Node.js application.", 43 | 2887)); 44 | add(new Product( 45 | "RESTful Web API Handbook", 46 | "This book is an exploration of the Restful web application-programming interface (API). The book begins by explaining what the API is, how it is used, and where it is used. The book then guides you on how to set up the various resources which are necessary for development in REST.", 47 | 1276)); 48 | add(new Product( 49 | "Building a RESTful Web Service with Spring", 50 | "Follow best practices and explore techniques such as clustering and caching to achieve a scalable web service\n" + 51 | "Leverage the Spring Framework to quickly implement RESTful endpoints\n" + 52 | "Learn to implement a client library for a RESTful web service using the Spring Framework", 53 | 2887)); 54 | }}; 55 | 56 | /** 57 | * Searches for products using a case-insensitive search term. 58 | * 59 | * @param searchTerm expression to search for 60 | * @return List of matching products, or an empty list. 61 | */ 62 | public List searchFor(final Optional searchTerm) { 63 | if (searchTerm.isPresent()) { 64 | return products 65 | .stream() 66 | .filter(matchingProductsFor(searchTerm.get())) 67 | .collect(toList()); 68 | } else { 69 | return products; 70 | } 71 | 72 | } 73 | 74 | /** 75 | * Searches for the product identified by {@code productId} 76 | * 77 | * @param productId nomen est omen 78 | * @return Optional product 79 | */ 80 | public Optional findBy(final String productId) { 81 | return products.stream().filter(p->p.id.equals(productId)).findAny(); 82 | } 83 | 84 | /** 85 | * @param searchTerm some search term 86 | * @return a Predicate used to match products against search terms. 87 | */ 88 | private Predicate matchingProductsFor(final String searchTerm) { 89 | return p -> 90 | p.title.toLowerCase().contains(searchTerm.toLowerCase()) 91 | || p.description.toLowerCase().contains(searchTerm.toLowerCase()); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/Curies.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Objects; 6 | import java.util.Optional; 7 | 8 | import static de.otto.edison.hal.CuriTemplate.matchingCuriTemplateFor; 9 | 10 | /** 11 | * Helper class used to resolve CURIs in links and embedded items. 12 | */ 13 | public class Curies { 14 | 15 | /** 16 | * The list of registered CURI links 17 | */ 18 | private final List curies; 19 | 20 | private Curies() { 21 | this.curies = new ArrayList<>(); 22 | } 23 | 24 | /** 25 | * Creates a Curies from a list of CURI links. 26 | * 27 | * @param curies list of {@link Link links} with link-relation type 'curies'. 28 | * @throws IllegalArgumentException if the list contains non-CURI links. 29 | * @since 1.0.0 30 | */ 31 | private Curies(final List curies) { 32 | this.curies = new ArrayList<>(); 33 | curies.forEach(this::register); 34 | } 35 | 36 | /** 37 | * Creates a clone from another Curies instance. 38 | * 39 | * @param other cloned Curies 40 | */ 41 | private Curies(final Curies other) { 42 | this.curies = new ArrayList<>(other.curies); 43 | } 44 | 45 | /** 46 | * Creates an empty Curies without curi links. 47 | * 48 | * @return default Curies 49 | */ 50 | public static Curies emptyCuries() { 51 | return new Curies(); 52 | } 53 | 54 | /** 55 | * Creates Curies from some {@link Links}. CURIes contained in the Links are 56 | * {@link #register(Link) registered}. 57 | * 58 | * @param links Links possibly containing CURIes 59 | * @return Curies 60 | */ 61 | public static Curies curies(final Links links) { 62 | List curies = links.getLinksBy("curies"); 63 | return curies(curies); 64 | } 65 | 66 | /** 67 | * Creates Curies from a list of CURI links. 68 | * 69 | * @param curies list of {@link Link links} with link-relation type 'curies'. 70 | * @return Curies 71 | * @throws IllegalArgumentException if the list contains non-CURI links. 72 | */ 73 | public static Curies curies(final List curies) { 74 | return new Curies(curies); 75 | } 76 | 77 | /** 78 | * Returns a copy of another Curies instance. 79 | * 80 | * @param other curies to copy 81 | * @return copied Curies instance 82 | */ 83 | public static Curies copyOf(final Curies other) { 84 | return new Curies(other); 85 | } 86 | 87 | /** 88 | * Registers a CURI link in the Curies instance. 89 | * 90 | * @param curi the CURI 91 | * @throws IllegalArgumentException if the link-relation type of the link is not equal to 'curies' 92 | */ 93 | public void register(final Link curi) { 94 | if (!curi.getRel().equals("curies")) { 95 | throw new IllegalArgumentException("Link must be a CURI"); 96 | } 97 | 98 | final boolean alreadyRegistered = curies 99 | .stream() 100 | .anyMatch(link -> link.getHref().equals(curi.getHref())); 101 | if (alreadyRegistered) { 102 | curies.removeIf(link -> link.getName().equals(curi.getName())); 103 | curies.replaceAll(link -> link.getName().equals(curi.getName()) ? curi : link); 104 | } 105 | curies.add(curi); 106 | } 107 | 108 | /** 109 | * Merges this Curies with another instance of Curies and returns the merged instance. 110 | * 111 | * @param other merged Curies 112 | * @return a merged copy of this and other 113 | */ 114 | public Curies mergeWith(final Curies other) { 115 | final Curies merged = copyOf(this); 116 | other.curies.forEach(merged::register); 117 | return merged; 118 | } 119 | 120 | /** 121 | * Resolves a link-relation type (curied or full rel) and returns the curied form, or 122 | * the unchanged rel, if no matching CURI is registered. 123 | * 124 | * @param rel link-relation type 125 | * @return curied link-relation type 126 | */ 127 | public String resolve(final String rel) { 128 | final Optional curiTemplate = matchingCuriTemplateFor(curies, rel); 129 | return curiTemplate.map(t -> t.curiedRelFrom(rel)).orElse(rel); 130 | } 131 | 132 | public String expand(final String rel) { 133 | if (rel.contains(":")) { 134 | final String name = rel.substring(0, rel.indexOf(":")); 135 | Optional curi = curies.stream().filter(c -> c.getName().equals(name)).findAny(); 136 | return curi.map(c -> c.getHrefAsTemplate() 137 | .set("rel", rel.substring(rel.indexOf(":")+1)) 138 | .expand()).orElse(rel); 139 | } else { 140 | return rel; 141 | } 142 | } 143 | 144 | public List getCuries() { 145 | return this.curies; 146 | } 147 | 148 | @Override 149 | public boolean equals(Object o) { 150 | if (this == o) return true; 151 | if (o == null || getClass() != o.getClass()) return false; 152 | Curies that = (Curies) o; 153 | return Objects.equals(curies, that.curies); 154 | } 155 | 156 | @Override 157 | public int hashCode() { 158 | 159 | return Objects.hash(curies); 160 | } 161 | 162 | @Override 163 | public String toString() { 164 | return "Curies{" + 165 | "curies=" + curies + 166 | '}'; 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/CuriesTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import org.junit.Test; 4 | 5 | import static de.otto.edison.hal.Link.curi; 6 | import static de.otto.edison.hal.Link.link; 7 | import static de.otto.edison.hal.Links.linkingTo; 8 | import static de.otto.edison.hal.Curies.emptyCuries; 9 | import static de.otto.edison.hal.Curies.curies; 10 | import static org.hamcrest.MatcherAssert.assertThat; 11 | import static org.hamcrest.Matchers.is; 12 | 13 | public class CuriesTest { 14 | 15 | @Test 16 | public void shouldBuildRegistryWithCuries() { 17 | // given 18 | final Curies curies = Curies.curies( 19 | linkingTo() 20 | .curi("x", "http://example.com/rels/{rel}") 21 | .curi("y", "http://example.org/rels/{rel}").build() 22 | ); 23 | // then 24 | assertThat(curies.resolve("http://example.com/rels/foo"), is("x:foo")); 25 | assertThat(curies.resolve("http://example.org/rels/bar"), is("y:bar")); 26 | } 27 | 28 | @Test 29 | public void shouldExpandFullRel() { 30 | // given 31 | final Curies curies = Curies.curies(linkingTo().curi("x", "http://example.com/rels/{rel}").build()); 32 | // when 33 | final String first = curies.expand("http://example.com/rels/foo"); 34 | final String second = curies.expand("item"); 35 | // then 36 | assertThat(first, is("http://example.com/rels/foo")); 37 | assertThat(second, is("item")); 38 | } 39 | 40 | @Test(expected = IllegalArgumentException.class) 41 | public void shouldFailToRegisterNonCuriLink() { 42 | emptyCuries().register(link("foo", "http://example.com/foo")); 43 | } 44 | 45 | @Test 46 | public void shouldResolveFullUri() { 47 | // given 48 | final Curies registry = emptyCuries(); 49 | registry.register(curi("o", "http://spec.otto.de/rels/{rel}")); 50 | // when 51 | final String resolved = registry.resolve("http://spec.otto.de/rels/foo"); 52 | // then 53 | assertThat(resolved, is("o:foo")); 54 | } 55 | 56 | @Test 57 | public void shouldResolveCuriedUri() { 58 | // given 59 | final Curies registry = emptyCuries(); 60 | registry.register(curi("o", "http://spec.otto.de/rels/{rel}")); 61 | // when 62 | final String resolved = registry.resolve("o:foo"); 63 | // then 64 | assertThat(resolved, is("o:foo")); 65 | } 66 | 67 | @Test 68 | public void shouldResolveUnknownFullUri() { 69 | // given 70 | final Curies registry = emptyCuries(); 71 | registry.register(curi("o", "http://spec.otto.de/rels/{rel}")); 72 | // when 73 | final String resolved = registry.resolve("http://www.otto.de/some/other"); 74 | // then 75 | assertThat(resolved, is("http://www.otto.de/some/other")); 76 | } 77 | 78 | @Test 79 | public void shouldResolveUnknownCuriedUri() { 80 | // given 81 | final Curies registry = emptyCuries(); 82 | registry.register(curi("o", "http://spec.otto.de/rels/{rel}")); 83 | // when 84 | final String resolved = registry.resolve("x:other"); 85 | // then 86 | assertThat(resolved, is("x:other")); 87 | } 88 | 89 | @Test 90 | public void shouldMergeRegistries() { 91 | // given 92 | final Curies registry = emptyCuries(); 93 | registry.register(curi("x", "http://x.otto.de/rels/{rel}")); 94 | final Curies other = emptyCuries(); 95 | other.register(curi("u", "http://u.otto.de/rels/{rel}")); 96 | // when 97 | final Curies merged = registry.mergeWith(other); 98 | // then 99 | assertThat(merged.resolve("http://x.otto.de/rels/foo"), is("x:foo")); 100 | assertThat(merged.resolve("http://u.otto.de/rels/foo"), is("u:foo")); 101 | } 102 | 103 | @Test 104 | public void shouldMergeByReplacingExistingWithOther() { 105 | // given 106 | final Curies registry = emptyCuries(); 107 | registry.register(curi("x", "http://x.otto.de/rels/{rel}")); 108 | final Curies other = emptyCuries(); 109 | other.register(curi("x", "http://spec.otto.de/rels/{rel}")); 110 | // when 111 | final Curies merged = registry.mergeWith(other); 112 | // then 113 | assertThat(merged.resolve("http://spec.otto.de/rels/foo"), is("x:foo")); 114 | } 115 | 116 | @Test 117 | public void shouldMergeEmptyRegistryWithNonEmpty() { 118 | // given 119 | final Curies empty = emptyCuries(); 120 | final Curies other = emptyCuries(); 121 | other.register(curi("o", "http://spec.otto.de/rels/{rel}")); 122 | // when 123 | final Curies merged = empty.mergeWith(other); 124 | // then 125 | assertThat(empty, is(emptyCuries())); 126 | assertThat(merged.resolve("http://spec.otto.de/rels/foo"), is("o:foo")); 127 | } 128 | 129 | @Test 130 | public void shouldExpandCuri() { 131 | // given 132 | final Curies curies = Curies.curies(linkingTo().curi("x", "http://example.com/rels/{rel}").build()); 133 | // when 134 | final String expanded = curies.expand("x:foo"); 135 | // then 136 | assertThat(expanded, is("http://example.com/rels/foo")); 137 | } 138 | 139 | @Test 140 | public void shouldReturnCuriIfNotResolvable() { 141 | // given 142 | final Curies curies = emptyCuries(); 143 | // when 144 | final String expanded = curies.expand("x:foo"); 145 | // then 146 | assertThat(expanded, is("x:foo")); 147 | } 148 | 149 | @Test 150 | public void shouldReturnCuriIfAlreadyResolved() { 151 | // given 152 | final Curies curies = Curies.curies(linkingTo().curi("x", "http://example.com/rels/{rel}").build()); 153 | // when 154 | final String expanded = curies.expand("http://example.com/rels/foo"); 155 | // then 156 | assertThat(expanded, is("http://example.com/rels/foo")); 157 | } 158 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/CuriTemplateTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import org.hamcrest.Description; 4 | import org.hamcrest.Matcher; 5 | import org.hamcrest.TypeSafeMatcher; 6 | import org.junit.Test; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | import static de.otto.edison.hal.CuriTemplate.curiTemplateFor; 12 | import static de.otto.edison.hal.CuriTemplate.matchingCuriTemplateFor; 13 | import static de.otto.edison.hal.Link.*; 14 | import static java.util.Arrays.asList; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | import static org.hamcrest.Matchers.is; 17 | import static org.junit.Assert.assertThrows; 18 | 19 | public class CuriTemplateTest { 20 | 21 | private final Link someCuri = curi("x", "http://example.org/rels/{rel}"); 22 | private final Link someOtherCuri = curi("y", "http://example.com/link-relations/{rel}"); 23 | 24 | private final List curies = asList(someCuri, someOtherCuri);; 25 | 26 | private final String someRel = "http://example.org/rels/product"; 27 | private final String someCuriedRel = "x:product"; 28 | 29 | private final String nonMatchingRel = "http://example.org/link-relations/product"; 30 | private final String nonMatchingCuriedRel = "z:product"; 31 | 32 | @Test 33 | public void shouldFailToCreateCuriTemplateForWrongRel() { 34 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 35 | curiTemplateFor(link("foo", "/bar")); 36 | }); 37 | assertThat(exception.getMessage(), matchesRegex(".*not a CURI link.*")); 38 | } 39 | 40 | @Test 41 | public void shouldFailToCreateCuriTemplateForWrongHref() { 42 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 43 | curiTemplateFor(linkBuilder("curies","/bar").withName("x").build()); 44 | }); 45 | assertThat(exception.getMessage(), matchesRegex(".*required.*placeholder.*")); 46 | } 47 | 48 | @Test 49 | public void shouldFailToCreateCuriTemplateWithMissingName() { 50 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 51 | curiTemplateFor(link("curies", "/bar/{rel}")); 52 | }); 53 | assertThat(exception.getMessage(), matchesRegex("Parameter is not a CURI link.")); 54 | } 55 | 56 | @Test 57 | public void shouldFindMatchingUriTemplateForExpandedRel() { 58 | final Optional curiTemplate = matchingCuriTemplateFor(curies, someRel); 59 | assertThat(curiTemplate.isPresent(), is(true)); 60 | assertThat(curiTemplate.get().getCuri(), is(someCuri)); 61 | } 62 | 63 | @Test 64 | public void shouldFindMatchingUriTemplateForCuriedRel() { 65 | final Optional curiTemplate = matchingCuriTemplateFor(curies, someCuriedRel); 66 | assertThat(curiTemplate.isPresent(), is(true)); 67 | assertThat(curiTemplate.get().getCuri(), is(someCuri)); 68 | } 69 | 70 | @Test 71 | public void shouldNotFindMatchingUriTemplate() { 72 | final Optional curiTemplate = matchingCuriTemplateFor(curies, nonMatchingRel); 73 | assertThat(curiTemplate.isPresent(), is(false)); 74 | } 75 | 76 | @Test 77 | public void shouldMatch() { 78 | assertThat(curiTemplateFor(someCuri).isMatching(someRel), is(true)); 79 | assertThat(curiTemplateFor(someCuri).isMatching(someCuriedRel), is(true)); 80 | assertThat(curiTemplateFor(someCuri).isMatchingExpandedRel(someRel), is(true)); 81 | assertThat(curiTemplateFor(someCuri).isMatchingCuriedRel(someCuriedRel), is(true)); 82 | } 83 | 84 | @Test 85 | public void shouldNotMatch() { 86 | assertThat(curiTemplateFor(someCuri).isMatching(nonMatchingRel), is(false)); 87 | assertThat(curiTemplateFor(someCuri).isMatching(nonMatchingCuriedRel), is(false)); 88 | assertThat(curiTemplateFor(someCuri).isMatchingExpandedRel(someCuriedRel), is(false)); 89 | assertThat(curiTemplateFor(someCuri).isMatchingCuriedRel(someRel), is(false)); 90 | } 91 | 92 | @Test 93 | public void shouldExtractCuriedRel() { 94 | assertThat(curiTemplateFor(someCuri).curiedRelFrom(someRel), is("x:product")); 95 | assertThat(curiTemplateFor(someCuri).curiedRelFrom(someCuriedRel), is("x:product")); 96 | } 97 | 98 | @Test 99 | public void shouldFailToExtractCuriedRelForNonMatchingRel() { 100 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 101 | curiTemplateFor(someCuri).curiedRelFrom(nonMatchingRel); 102 | }); 103 | assertThat(exception.getMessage(), matchesRegex("Rel does not match the CURI template.")); 104 | } 105 | 106 | @Test 107 | public void shouldExpandRel() { 108 | assertThat(curiTemplateFor(someCuri).expandedRelFrom(someRel), is(someRel)); 109 | assertThat(curiTemplateFor(someCuri).expandedRelFrom(someCuriedRel), is(someRel)); 110 | } 111 | 112 | @Test 113 | public void shouldFailToExpandNonMatchingRel() { 114 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 115 | curiTemplateFor(someCuri).expandedRelFrom(nonMatchingRel); 116 | }); 117 | assertThat(exception.getMessage(), matchesRegex("Rel does not match the CURI template.")); 118 | } 119 | 120 | @Test 121 | public void shouldExtractPlaceholderValue() { 122 | assertThat(curiTemplateFor(someCuri).relPlaceHolderFrom(someRel), is("product")); 123 | assertThat(curiTemplateFor(someCuri).relPlaceHolderFrom(someCuriedRel), is("product")); 124 | } 125 | 126 | @Test 127 | public void shouldFailToExtractPlaceholderRelForNonMatchingRel() { 128 | IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { 129 | curiTemplateFor(someCuri).relPlaceHolderFrom(nonMatchingRel); 130 | }); 131 | assertThat(exception.getMessage(), matchesRegex("Rel does not match the CURI template.")); 132 | } 133 | 134 | private Matcher matchesRegex(final String regex) { 135 | return new TypeSafeMatcher() { 136 | @Override 137 | public void describeTo(Description description) { 138 | description.appendText("not matching " + regex); 139 | } 140 | 141 | @Override 142 | protected boolean matchesSafely(final String item) { 143 | return item.matches(regex); 144 | } 145 | }; 146 | } 147 | } -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/EmbeddedTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import org.junit.Test; 4 | 5 | import java.util.List; 6 | 7 | import static de.otto.edison.hal.Embedded.Builder.copyOf; 8 | import static de.otto.edison.hal.Embedded.*; 9 | import static de.otto.edison.hal.Link.curi; 10 | import static de.otto.edison.hal.Link.link; 11 | import static de.otto.edison.hal.Links.linkingTo; 12 | import static java.util.Arrays.asList; 13 | import static java.util.Collections.emptyList; 14 | import static java.util.Collections.singletonList; 15 | import static org.hamcrest.MatcherAssert.assertThat; 16 | import static org.hamcrest.Matchers.*; 17 | 18 | 19 | public class EmbeddedTest { 20 | 21 | @Test 22 | public void shouldCreateEmptyEmbedded() { 23 | Embedded embedded = emptyEmbedded(); 24 | assertThat(embedded.getItemsBy("foo"), is(emptyList())); 25 | assertThat(embedded.getItemsBy("foo", HalRepresentation.class), is(emptyList())); 26 | } 27 | 28 | @Test 29 | public void shouldCreateEmbedded() { 30 | Embedded embedded = embedded("foo", singletonList(new HalRepresentation())); 31 | assertThat(embedded.getItemsBy("foo"), hasSize(1)); 32 | } 33 | 34 | @Test 35 | public void shouldCreateSingleEmbedded() { 36 | Embedded embedded = embedded("foo", new HalRepresentation()); 37 | assertThat(embedded.getItemsBy("foo"), hasSize(1)); 38 | } 39 | 40 | @Test 41 | public void shouldCreateEmbeddedAsArray() { 42 | Embedded embedded = embedded("foo", singletonList(new HalRepresentation())); 43 | assertThat(embedded.hasItem("foo"), is(true)); 44 | assertThat(embedded.isArray("foo"), is(true)); 45 | } 46 | 47 | @Test 48 | public void shouldCreateEmbeddedAsSingleObject() { 49 | Embedded embedded = embedded("foo", new HalRepresentation()); 50 | assertThat(embedded.hasItem("foo"), is(true)); 51 | assertThat(embedded.isArray("foo"), is(false)); 52 | } 53 | 54 | @Test 55 | public void shouldCreateEmbeddedWithBuilder() { 56 | Embedded embedded = embeddedBuilder() 57 | .with("foo", singletonList(new HalRepresentation())) 58 | .with("bar", singletonList(new HalRepresentation())) 59 | .build(); 60 | assertThat(embedded.getItemsBy("foo"), hasSize(1)); 61 | assertThat(embedded.getItemsBy("bar"), hasSize(1)); 62 | assertThat(embedded.getItemsBy("foobar"), hasSize(0)); 63 | } 64 | 65 | @Test 66 | public void shouldAddRelToEmbeddedUsingBuilder() { 67 | Embedded embedded = copyOf(embedded("foo", singletonList(new HalRepresentation()))) 68 | .with("bar", singletonList(new HalRepresentation())).build(); 69 | assertThat(embedded.getItemsBy("foo"), hasSize(1)); 70 | assertThat(embedded.getItemsBy("bar"), hasSize(1)); 71 | assertThat(embedded.getItemsBy("foobar"), hasSize(0)); 72 | } 73 | 74 | @Test 75 | public void shouldGetItemsAsType() { 76 | Embedded embedded = embedded("foo", asList( 77 | new TestHalRepresentation(), 78 | new TestHalRepresentation()) 79 | ); 80 | final List testHalRepresentations = embedded.getItemsBy("foo", TestHalRepresentation.class); 81 | assertThat(testHalRepresentations, hasSize(2)); 82 | assertThat(testHalRepresentations.get(0), instanceOf(TestHalRepresentation.class)); 83 | } 84 | 85 | @Test 86 | public void shouldGetAllLinkrelations() { 87 | Embedded embedded = embeddedBuilder() 88 | .with("foo", singletonList(new HalRepresentation())) 89 | .with("bar", singletonList(new HalRepresentation())) 90 | .build(); 91 | assertThat(embedded.getRels(), contains("foo", "bar")); 92 | } 93 | 94 | @Test 95 | public void shouldReplaceRelsWithCuriedRels() { 96 | Curies curies = Curies.curies(asList( 97 | curi("test", "http://example.com/rels/{rel}")) 98 | ); 99 | Embedded embedded = embeddedBuilder() 100 | .with("http://example.com/rels/foo", singletonList(new HalRepresentation())) 101 | .with("http://example.com/rels/bar", singletonList(new HalRepresentation())) 102 | .build(); 103 | assertThat(embedded.using(curies).getRels(), contains("test:foo", "test:bar")); 104 | } 105 | 106 | @Test 107 | public void shouldReplaceNestedRelsWithCuriedRels() { 108 | Curies curies = Curies.curies(asList( 109 | curi("test", "http://example.com/rels/{rel}")) 110 | ); 111 | Embedded embedded = embeddedBuilder() 112 | .with("http://example.com/rels/foo", singletonList( 113 | new HalRepresentation(null, 114 | embeddedBuilder() 115 | .with("http://example.com/rels/bar", singletonList(new HalRepresentation())) 116 | .build()))) 117 | .using(curies) 118 | .build(); 119 | assertThat(embedded.getRels(), contains("test:foo")); 120 | assertThat(embedded.getItemsBy("test:foo").get(0).getEmbedded().getRels(), contains("test:bar")); 121 | } 122 | 123 | @Test 124 | public void shouldReplaceNestedLinkRelsWithCuriedLinkRels() { 125 | Curies curies = Curies.curies(asList( 126 | curi("test", "http://example.com/rels/{rel}")) 127 | ); 128 | Embedded embedded = embeddedBuilder() 129 | .with("http://example.com/rels/foo", singletonList( 130 | new HalRepresentation( 131 | linkingTo() 132 | .single(link("http://example.com/rels/bar", "http://example.com")) 133 | .build() 134 | ))) 135 | .using(curies) 136 | .build(); 137 | assertThat(embedded.getItemsBy("test:foo").get(0).getLinks().getRels(), contains("test:bar")); 138 | } 139 | 140 | @Test 141 | public void shouldReplaceRelsWithCuriedRelsUsingBuilder() { 142 | Curies curies = Curies.curies(asList( 143 | curi("test", "http://example.com/rels/{rel}")) 144 | ); 145 | Embedded embedded = embeddedBuilder() 146 | .with("http://example.com/rels/foo", singletonList(new HalRepresentation())) 147 | .with("http://example.com/rels/bar", singletonList(new HalRepresentation())) 148 | .using(curies) 149 | .build(); 150 | assertThat(embedded.getRels(), contains("test:foo", "test:bar")); 151 | } 152 | 153 | static class TestHalRepresentation extends HalRepresentation { 154 | } 155 | } -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/LinksBuilderTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import org.junit.Test; 4 | 5 | import static de.otto.edison.hal.Link.*; 6 | import static de.otto.edison.hal.Links.copyOf; 7 | import static de.otto.edison.hal.Links.linkingTo; 8 | import static java.util.Arrays.asList; 9 | import static org.hamcrest.MatcherAssert.assertThat; 10 | import static org.hamcrest.Matchers.*; 11 | 12 | public class LinksBuilderTest { 13 | 14 | @Test 15 | public void shouldAddLinksWithNewRel() { 16 | final Links links = linkingTo().self("/foo").build(); 17 | final Links extendedLinks = copyOf(links) 18 | .with(linkingTo().item("/bar").build()) 19 | .build(); 20 | assertThat(extendedLinks.getLinkBy("self").isPresent(), is(true)); 21 | assertThat(extendedLinks.getLinkBy("item").isPresent(), is(true)); 22 | } 23 | 24 | @Test 25 | public void shouldAddLinkWithNewRel() { 26 | final Links links = linkingTo().self("/foo").build(); 27 | final Links extendedLinks = copyOf(links) 28 | .array(item("/bar")) 29 | .build(); 30 | assertThat(extendedLinks.getLinkBy("self").isPresent(), is(true)); 31 | assertThat(extendedLinks.getLinkBy("item").isPresent(), is(true)); 32 | } 33 | 34 | @Test 35 | public void shouldAddItemLink() { 36 | final Links links = linkingTo() 37 | .item("/bar") 38 | .build(); 39 | assertThat(links.getLinkBy("item").isPresent(), is(true)); 40 | assertThat(links.getLinkBy("item").get().getHref(), is("/bar")); 41 | } 42 | 43 | @Test 44 | public void shouldAddCollectionLink() { 45 | final Links links = linkingTo() 46 | .collection("/") 47 | .build(); 48 | assertThat(links.getLinkBy("collection").isPresent(), is(true)); 49 | assertThat(links.getLinkBy("collection").get().getHref(), is("/")); 50 | } 51 | 52 | @Test 53 | public void shouldAddTemplatedCollectionLink() { 54 | final Links links = linkingTo().self("/foo").build(); 55 | final Links extendedLinks = copyOf(links) 56 | .collection("/{?q}") 57 | .array(item("/bar")) 58 | .build(); 59 | assertThat(extendedLinks.getLinkBy("collection").isPresent(), is(true)); 60 | assertThat(extendedLinks.getLinkBy("collection").get().getHref(), is("/{?q}")); 61 | assertThat(extendedLinks.getLinkBy("collection").get().isTemplated(), is(true)); 62 | assertThat(extendedLinks.getLinkBy("collection").get().getHrefAsTemplate().set("q", "foo").expand(), is("/?q=foo")); 63 | } 64 | 65 | @Test 66 | public void shouldAddLinkToExistingItemRel() { 67 | final Links links = linkingTo().item("/foo").build(); 68 | final Links extendedLinks = copyOf(links) 69 | .array(item("/bar")) 70 | .build(); 71 | assertThat(extendedLinks.getLinksBy("item"), hasSize(2)); 72 | } 73 | 74 | @Test 75 | public void shouldAddLinkToExistingArrayRel() { 76 | final Links links = linkingTo().array(link("some-rel", "/foo")).build(); 77 | final Links extendedLinks = copyOf(links) 78 | .array(link("some-rel", "/bar")) 79 | .build(); 80 | assertThat(extendedLinks.getLinksBy("some-rel"), hasSize(2)); 81 | } 82 | 83 | @Test(expected = IllegalStateException.class) 84 | public void shouldNotAddLinkToExistingArrayRelUsingSingle() { 85 | final Links links = linkingTo().array(link("some-rel", "/foo")).build(); 86 | copyOf(links) 87 | .single(link("some-rel", "/bar")) 88 | .build(); 89 | } 90 | 91 | @Test(expected = IllegalStateException.class) 92 | public void shouldFailToAddLinkToExistingSingleRel() { 93 | final Links links = linkingTo().single(link("some-rel", "/foo")).build(); 94 | copyOf(links) 95 | .array(link("some-rel", "/bar")) 96 | .build(); 97 | } 98 | 99 | @Test 100 | public void shouldAddLinksToExistingArrayRel() { 101 | final Links links = linkingTo().array(link("some-rel", "/foo")).build(); 102 | final Links extendedLinks = copyOf(links) 103 | .array(asList(item("/bar"), item("/foobar"))) 104 | .build(); 105 | assertThat(extendedLinks.getLinksBy("item"), hasSize(2)); 106 | } 107 | 108 | @Test 109 | public void shouldNotDuplicateLink() { 110 | final Links links = linkingTo().item("/foo").item("/bar").build(); 111 | final Links extendedLinks = copyOf(links) 112 | .array(item("/bar")) 113 | .build(); 114 | assertThat(extendedLinks.getLinksBy("item"), hasSize(2)); 115 | } 116 | 117 | @Test 118 | public void shouldAddDifferentButNotEquivalentLinks() { 119 | final Links links = linkingTo() 120 | .array(linkBuilder("myrel", "/foo") 121 | .withType("some type") 122 | .withProfile("some profile") 123 | .build()) 124 | .build(); 125 | final Links extendedLinks = copyOf(links) 126 | .array(linkBuilder("myrel", "/foo") 127 | .withType("some DIFFERENT type") 128 | .withProfile("some profile") 129 | .withTitle("ignored title") 130 | .withDeprecation("ignored deprecation") 131 | .withHrefLang("ignored language") 132 | .withName("ignored name") 133 | .build()) 134 | .build(); 135 | assertThat(extendedLinks.getLinksBy("myrel"), hasSize(2)); 136 | } 137 | 138 | @Test 139 | public void shouldNotAddEquivalentLinks() { 140 | final Links links = linkingTo() 141 | .array(linkBuilder("myrel", "/foo") 142 | .withType("some type") 143 | .withProfile("some profile") 144 | .build()) 145 | .build(); 146 | final Links extendedLinks = copyOf(links) 147 | .array(linkBuilder("myrel", "/foo") 148 | .withType("some type") 149 | .withProfile("some profile") 150 | .withName("foo") 151 | .build()) 152 | .build(); 153 | assertThat(extendedLinks.getLinksBy("myrel"), hasSize(1)); 154 | } 155 | 156 | @Test 157 | public void shouldMergeCuries() { 158 | final Links.Builder someLinks = Links.linkingTo().curi("x", "http://example.com/rels/{rel}"); 159 | final Links otherLinks = Links.linkingTo().curi("y", "http://example.org/rels/{rel}").build(); 160 | 161 | final Links mergedLinks = someLinks.with(otherLinks).build(); 162 | 163 | assertThat(mergedLinks.getLinksBy("curies"), contains( 164 | curi("x", "http://example.com/rels/{rel}"), 165 | curi("y", "http://example.org/rels/{rel}"))); 166 | } 167 | } -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/HalRepresentationCuriesTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.junit.Test; 6 | 7 | import static de.otto.edison.hal.Curies.curies; 8 | import static de.otto.edison.hal.Embedded.embedded; 9 | import static de.otto.edison.hal.Embedded.emptyEmbedded; 10 | import static de.otto.edison.hal.Link.curi; 11 | import static de.otto.edison.hal.Link.link; 12 | import static de.otto.edison.hal.Links.emptyLinks; 13 | import static de.otto.edison.hal.Links.linkingTo; 14 | import static java.util.Arrays.asList; 15 | import static java.util.Collections.singletonList; 16 | import static org.hamcrest.MatcherAssert.assertThat; 17 | import static org.hamcrest.Matchers.*; 18 | 19 | public class HalRepresentationCuriesTest { 20 | 21 | @Test 22 | public void shouldRenderSingleCuriAsArray() throws JsonProcessingException { 23 | // given 24 | final HalRepresentation representation = new HalRepresentation( 25 | linkingTo() 26 | .curi("x", "http://example.org/rels/{rel}") 27 | .single( 28 | link("x:foo", "http://example.org/test"), 29 | link("x:bar", "http://example.org/test")) 30 | .build() 31 | ); 32 | // when 33 | final String json = new ObjectMapper().writeValueAsString(representation); 34 | // then 35 | assertThat(json, is("{\"_links\":{\"curies\":[{\"href\":\"http://example.org/rels/{rel}\",\"templated\":true,\"name\":\"x\"}],\"x:foo\":{\"href\":\"http://example.org/test\"},\"x:bar\":{\"href\":\"http://example.org/test\"}}}")); 36 | } 37 | 38 | @Test 39 | public void shouldRenderCuries() throws JsonProcessingException { 40 | // given 41 | final HalRepresentation representation = new HalRepresentation( 42 | linkingTo() 43 | .curi("x", "http://example.org/rels/{rel}") 44 | .curi("y", "http://example.com/rels/{rel}") 45 | .single(link("x:foo", "http://example.org/test")) 46 | .single(link("y:bar", "http://example.org/test")) 47 | .build() 48 | ); 49 | // when 50 | final String json = new ObjectMapper().writeValueAsString(representation); 51 | // then 52 | assertThat(json, is("{\"_links\":{\"curies\":[{\"href\":\"http://example.org/rels/{rel}\",\"templated\":true,\"name\":\"x\"},{\"href\":\"http://example.com/rels/{rel}\",\"templated\":true,\"name\":\"y\"}],\"x:foo\":{\"href\":\"http://example.org/test\"},\"y:bar\":{\"href\":\"http://example.org/test\"}}}")); 53 | } 54 | 55 | @Test 56 | public void shouldReplaceFullRelWithCuri() throws JsonProcessingException { 57 | // given 58 | final HalRepresentation representation = new HalRepresentation( 59 | linkingTo() 60 | .curi("x", "http://example.org/rels/{rel}") 61 | .single(link("http://example.org/rels/foo", "http://example.org/test")) 62 | .build() 63 | ); 64 | // when 65 | final String json = new ObjectMapper().writeValueAsString(representation); 66 | // then 67 | assertThat(json, is("{\"_links\":{\"curies\":[{\"href\":\"http://example.org/rels/{rel}\",\"templated\":true,\"name\":\"x\"}],\"x:foo\":{\"href\":\"http://example.org/test\"}}}")); 68 | } 69 | 70 | @Test 71 | public void shouldConstructWithCuries() { 72 | final Curies curies = curies(asList(curi("x", "http://example.com/rels/{rel}"))); 73 | final HalRepresentation hal = new HalRepresentation(emptyLinks(), emptyEmbedded(), curies); 74 | assertThat(hal.getCuries().resolve("http://example.com/rels/foo"), is("x:foo")); 75 | } 76 | 77 | @Test 78 | public void shouldConstructWithLinksAndCuries() { 79 | final Curies curies = curies(asList(curi("x", "http://example.com/rels/{rel}"))); 80 | final HalRepresentation hal = new HalRepresentation( 81 | linkingTo().single(link("http://example.com/rels/foo", "http://example.com")).build(), 82 | emptyEmbedded(), 83 | curies); 84 | assertThat(hal.getLinks().getRels(), contains("x:foo")); 85 | assertThat(hal.getLinks().getLinkBy("x:foo").isPresent(), is(true)); 86 | assertThat(hal.getLinks().getLinkBy("http://example.com/rels/foo").isPresent(), is(true)); 87 | } 88 | 89 | @Test 90 | public void shouldConstructWithEmbeddedAndCuries() { 91 | final Curies curies = curies(asList(curi("x", "http://example.com/rels/{rel}"))); 92 | final HalRepresentation hal = new HalRepresentation( 93 | emptyLinks(), 94 | embedded( 95 | "http://example.com/rels/nested", 96 | singletonList(new HalRepresentation( 97 | linkingTo().single(link("http://example.com/rels/foo", "http://example.com")).build(), 98 | emptyEmbedded() 99 | ))), 100 | curies); 101 | assertThat(hal.getEmbedded().getRels(), contains("x:nested")); 102 | final HalRepresentation embedded = hal.getEmbedded().getItemsBy("http://example.com/rels/nested").get(0); 103 | assertThat(embedded.getLinks().getLinkBy("x:foo").isPresent(), is(true)); 104 | assertThat(embedded.getLinks().getLinkBy("http://example.com/rels/foo").isPresent(), is(true)); 105 | } 106 | 107 | @Test 108 | public void shouldInheritCuries() { 109 | final HalRepresentation embeddedHal = new HalRepresentation(); 110 | final HalRepresentation representation = new HalRepresentation(emptyLinks(), embedded("http://example.com/rels/foo", singletonList(embeddedHal))); 111 | representation.mergeWithEmbedding(curies(singletonList(curi("x", "http://example.com/rels/{rel}")))); 112 | assertThat(embeddedHal.getCuries().resolve("http://example.com/rels/foo"), is("x:foo")); 113 | } 114 | 115 | @Test 116 | public void shouldRemoveDuplicateCuries() { 117 | // given 118 | final HalRepresentation embeddedHal = new HalRepresentation(linkingTo().curi("x", "http://example.com/rels/{rel}").build()); 119 | final HalRepresentation representation = new HalRepresentation(linkingTo().curi("x", "http://example.com/rels/{rel}").build(), embedded("http://example.com/rels/foo", singletonList(embeddedHal))); 120 | // when 121 | final HalRepresentation embeddedAfterCreation = representation.getEmbedded().getItemsBy("x:foo").get(0); 122 | // then 123 | assertThat(embeddedAfterCreation.getCuries().resolve("http://example.com/rels/foo"), is("x:foo")); 124 | assertThat(embeddedAfterCreation.getLinks().getLinksBy("curies"), is(empty())); 125 | assertThat(representation.getLinks().getLinksBy("curies"), contains(curi("x", "http://example.com/rels/{rel}"))); 126 | // but 127 | assertThat(embeddedHal.getLinks().getLinksBy("curies"), is(empty())); 128 | } 129 | 130 | } 131 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/LinkTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import org.junit.Test; 5 | 6 | import static de.otto.edison.hal.Link.*; 7 | import static de.otto.edison.hal.Links.linkingTo; 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.hasSize; 10 | import static org.hamcrest.Matchers.is; 11 | 12 | public class LinkTest { 13 | 14 | @Test 15 | public void shouldBuildSelfLink() { 16 | final Link self = self("http://example.org"); 17 | assertThat(self.getRel(), is("self")); 18 | assertThat(self.getHref(), is("http://example.org")); 19 | } 20 | 21 | @Test 22 | public void shouldBuildProfileLink() { 23 | final Link profile = profile("http://example.org/profiles/test"); 24 | assertThat(profile.getRel(), is("profile")); 25 | assertThat(profile.getHref(), is("http://example.org/profiles/test")); 26 | } 27 | 28 | @Test 29 | public void shouldBuildItemLink() { 30 | final Link self = item("http://example.org/items/42"); 31 | assertThat(self.getRel(), is("item")); 32 | assertThat(self.getHref(), is("http://example.org/items/42")); 33 | } 34 | 35 | @Test 36 | public void shouldBuildCollectionLink() { 37 | final Link self = collection("http://example.org/items"); 38 | assertThat(self.getRel(), is("collection")); 39 | assertThat(self.getHref(), is("http://example.org/items")); 40 | } 41 | 42 | @Test 43 | public void shouldBuildTemplatedLink() { 44 | final Link link = link("myRel", "/test/{foo}"); 45 | assertThat(link.getRel(), is("myRel")); 46 | assertThat(link.getHref(), is("/test/{foo}")); 47 | assertThat(link.isTemplated(), is(true)); 48 | assertThat(link.getHrefAsTemplate().set("foo", 42).expand(), is("/test/42")); 49 | } 50 | 51 | @Test 52 | public void shouldBuildTemplatedLinkUsingLinkBuilder() { 53 | final Link self = linkBuilder("myRel", "/test/{foo}") 54 | .withHrefLang("de_DE") 55 | .withTitle("title") 56 | .withName("name") 57 | .withProfile("my-profile") 58 | .withType("type") 59 | .build(); 60 | assertThat(self.getRel(), is("myRel")); 61 | assertThat(self.getHref(), is("/test/{foo}")); 62 | assertThat(self.isTemplated(), is(true)); 63 | assertThat(self.getHreflang(), is("de_DE")); 64 | assertThat(self.getTitle(), is("title")); 65 | assertThat(self.getName(), is("name")); 66 | assertThat(self.getType(), is("type")); 67 | assertThat(self.getProfile(), is("my-profile")); 68 | assertThat(self.getDeprecation(), is("")); 69 | } 70 | 71 | @Test 72 | public void shouldBuildLinkUsingBuilder() { 73 | final Link link = linkBuilder("myRel", "/test/foo") 74 | .withHrefLang("de_DE") 75 | .withTitle("title") 76 | .withName("name") 77 | .withProfile("my-profile") 78 | .withType("type") 79 | .build(); 80 | assertThat(link.getRel(), is("myRel")); 81 | assertThat(link.getHref(), is("/test/foo")); 82 | assertThat(link.isTemplated(), is(false)); 83 | assertThat(link.getHreflang(), is("de_DE")); 84 | assertThat(link.getTitle(), is("title")); 85 | assertThat(link.getName(), is("name")); 86 | assertThat(link.getType(), is("type")); 87 | assertThat(link.getProfile(), is("my-profile")); 88 | } 89 | 90 | @Test 91 | public void shouldBuildDeprecatedLink() { 92 | final Link link = linkBuilder("myRel", "/test/foo") 93 | .withDeprecation("http://example.com/whyThisIsDeprecated.html") 94 | .build(); 95 | assertThat(link.getDeprecation(), is("http://example.com/whyThisIsDeprecated.html")); 96 | } 97 | 98 | @Test 99 | public void shouldHaveProperEqualsAndHashCode() { 100 | final Link link1 = linkBuilder("myRel", "/test/foo") 101 | .withHrefLang("de_DE") 102 | .withTitle("title") 103 | .withName("name") 104 | .withProfile("my-profile") 105 | .withType("type") 106 | .build(); 107 | final Link link2 = linkBuilder("myRel", "/test/foo") 108 | .withHrefLang("de_DE") 109 | .withTitle("title") 110 | .withName("name") 111 | .withProfile("my-profile") 112 | .withType("type") 113 | .build(); 114 | assertThat(link1, is(link2)); 115 | assertThat(link2, is(link1)); 116 | assertThat(link1.hashCode(), is(link2.hashCode())); 117 | } 118 | 119 | @Test 120 | public void shouldBuildCuri() throws JsonProcessingException { 121 | final Link link = curi("t", "http://example.org/{rel}"); 122 | assertThat(link.getName(), is("t")); 123 | assertThat(link.getRel(), is("curies")); 124 | assertThat(link.isTemplated(), is(true)); 125 | } 126 | 127 | @Test(expected = IllegalArgumentException.class) 128 | public void shouldFailToBuildCuriWithoutRelPlaceholder() throws JsonProcessingException { 129 | curi("t", "http://example.org/rel"); 130 | } 131 | 132 | @Test 133 | public void shouldBeEquivalent() { 134 | final Link first = linkBuilder("myrel", "/foo") 135 | .withType("some type") 136 | .withProfile("some profile") 137 | .build(); 138 | final Link other = linkBuilder("myrel", "/foo") 139 | .withType("some type") 140 | .withProfile("some profile") 141 | .withTitle("ignored title") 142 | .withDeprecation("ignored deprecation") 143 | .withHrefLang("ignored language") 144 | .withName("ignored name") 145 | .build(); 146 | assertThat(first.isEquivalentTo(other), is(true)); 147 | } 148 | 149 | @Test 150 | public void shouldNotBeEquivalentIfHrefIsDifferent() { 151 | final Link first = link("myrel", "/foo"); 152 | final Link other = link("myrel", "/bar"); 153 | assertThat(first.isEquivalentTo(other), is(false)); 154 | } 155 | 156 | @Test 157 | public void shouldNotBeEquivalentIfRelIsDifferent() { 158 | final Link first = link("myrel", "/foo"); 159 | final Link other = link("myOtherRel", "/foo"); 160 | assertThat(first.isEquivalentTo(other), is(false)); 161 | } 162 | 163 | @Test 164 | public void shouldNotBeEquivalentIfTypeIsDifferent() { 165 | final Link first = linkBuilder("myrel", "/foo") 166 | .withType("some type") 167 | .build(); 168 | final Link other = linkBuilder("myrel", "/foo") 169 | .withType("some other type") 170 | .build(); 171 | assertThat(first.isEquivalentTo(other), is(false)); 172 | } 173 | 174 | @Test 175 | public void shouldNotBeEquivalentIfProfileIsDifferent() { 176 | final Link first = linkBuilder("myrel", "/foo") 177 | .withType("some profile") 178 | .build(); 179 | final Link other = linkBuilder("myrel", "/foo") 180 | .withType("some other profile") 181 | .build(); 182 | assertThat(first.isEquivalentTo(other), is(false)); 183 | } 184 | 185 | } -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/paging/ZeroBasedNumberedPagingTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.paging; 2 | 3 | import com.damnhandy.uri.template.UriTemplate; 4 | import de.otto.edison.hal.Links; 5 | import org.junit.Test; 6 | 7 | import java.util.EnumSet; 8 | 9 | import static com.damnhandy.uri.template.UriTemplate.fromTemplate; 10 | import static de.otto.edison.hal.paging.NumberedPaging.zeroBasedNumberedPaging; 11 | import static de.otto.edison.hal.paging.PagingRel.*; 12 | import static java.lang.Integer.MAX_VALUE; 13 | import static java.util.EnumSet.*; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.is; 16 | 17 | public class ZeroBasedNumberedPagingTest { 18 | 19 | public static final UriTemplate URI_TEMPLATE = fromTemplate("/{?page,pageSize}"); 20 | 21 | public static final EnumSet ALL_RELS = allOf(PagingRel.class); 22 | 23 | @Test(expected = IllegalArgumentException.class) 24 | public void shouldFailToSkipNegativePageNum1() { 25 | zeroBasedNumberedPaging(-1, 2, true); 26 | } 27 | 28 | @Test(expected = IllegalArgumentException.class) 29 | public void shouldFailToSkipNegativePageNum2() { 30 | zeroBasedNumberedPaging(-1, 2, 42); 31 | } 32 | 33 | @Test(expected = IllegalArgumentException.class) 34 | public void shouldFailToLimitPageSize1() { 35 | zeroBasedNumberedPaging(0, 0, true); 36 | } 37 | 38 | @Test(expected = IllegalArgumentException.class) 39 | public void shouldFailToLimitPageSize2() { 40 | zeroBasedNumberedPaging(0, 0, 42); 41 | } 42 | 43 | @Test(expected = IllegalArgumentException.class) 44 | public void shouldFailToProvideMoreElements() { 45 | zeroBasedNumberedPaging(0, Integer.MAX_VALUE, true); 46 | } 47 | 48 | @Test(expected = IllegalArgumentException.class) 49 | public void shouldFailToHaveTotalCountLessThenZero() { 50 | zeroBasedNumberedPaging(0, 4, -1); 51 | } 52 | 53 | @Test 54 | public void shouldHandleEmptyPage() { 55 | final NumberedPaging p = zeroBasedNumberedPaging(0, 100, 0); 56 | 57 | assertThat(p.getPageNumber(), is(0)); 58 | assertThat(p.getPageSize(), is(100)); 59 | assertThat(p.getTotal().getAsInt(), is(0)); 60 | assertThat(p.getLastPage().getAsInt(), is(0)); 61 | assertThat(p.hasMore(), is(false)); 62 | } 63 | 64 | @Test 65 | public void shouldHandleMostlyEmptySinglePage() { 66 | final NumberedPaging p = zeroBasedNumberedPaging(0, 100, 1); 67 | 68 | assertThat(p.getPageNumber(), is(0)); 69 | assertThat(p.getPageSize(), is(100)); 70 | assertThat(p.getTotal().getAsInt(), is(1)); 71 | assertThat(p.getLastPage().getAsInt(), is(0)); 72 | assertThat(p.hasMore(), is(false)); 73 | } 74 | 75 | @Test 76 | public void shouldHandleFullSinglePage() { 77 | final NumberedPaging p = zeroBasedNumberedPaging(0, 100, 100); 78 | 79 | assertThat(p.getPageNumber(), is(0)); 80 | assertThat(p.getPageSize(), is(100)); 81 | assertThat(p.getTotal().getAsInt(), is(100)); 82 | assertThat(p.getLastPage().getAsInt(), is(0)); 83 | assertThat(p.hasMore(), is(false)); 84 | } 85 | 86 | @Test 87 | public void shouldHandleMultiplePages() { 88 | final NumberedPaging p = zeroBasedNumberedPaging(0, 100, 201); 89 | 90 | assertThat(p.getPageNumber(), is(0)); 91 | assertThat(p.getPageSize(), is(100)); 92 | assertThat(p.getTotal().getAsInt(), is(201)); 93 | assertThat(p.getLastPage().getAsInt(), is(2)); 94 | assertThat(p.hasMore(), is(true)); 95 | } 96 | 97 | @Test 98 | public void shouldBuildLinksForEmptyPage() { 99 | Links paging = zeroBasedNumberedPaging(0, 100, 0).links(URI_TEMPLATE, ALL_RELS); 100 | 101 | assertThat(hrefFrom(paging, "self"), is("/?page=0&pageSize=100")); 102 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=100")); 103 | assertThat(paging.getLinkBy("next").isPresent(), is(false)); 104 | assertThat(paging.getLinkBy("prev").isPresent(), is(false)); 105 | assertThat(hrefFrom(paging, "last"), is("/?page=0&pageSize=100")); 106 | } 107 | 108 | @Test 109 | public void shouldOnlyBuildWantedLinks() { 110 | Links paging = zeroBasedNumberedPaging(3, 3, 10).links(URI_TEMPLATE, range(PREV, NEXT)); 111 | 112 | assertThat(isAbsent(paging, "self"), is(true)); 113 | assertThat(isAbsent(paging, "first"), is(true)); 114 | assertThat(isAbsent(paging, "next"), is(true)); 115 | assertThat(isAbsent(paging, "prev"), is(false)); 116 | assertThat(isAbsent(paging, "last"), is(true)); 117 | } 118 | 119 | 120 | @Test 121 | public void shouldBuildUriWithoutParams() { 122 | Links paging = zeroBasedNumberedPaging(0, MAX_VALUE, false).links(URI_TEMPLATE, ALL_RELS); 123 | 124 | assertThat(hrefFrom(paging, "self"), is("/")); 125 | assertThat(hrefFrom(paging, "first"), is("/")); 126 | assertThat(paging.getLinkBy("next").isPresent(), is(false)); 127 | assertThat(paging.getLinkBy("prev").isPresent(), is(false)); 128 | assertThat(paging.getLinkBy("last").isPresent(), is(false)); 129 | } 130 | 131 | @Test 132 | public void shouldBuildUrisForFirstPage() { 133 | Links paging = zeroBasedNumberedPaging(0, 2, true).links(URI_TEMPLATE, ALL_RELS); 134 | 135 | assertThat(hrefFrom(paging, "self"), is("/?page=0&pageSize=2")); 136 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=2")); 137 | assertThat(hrefFrom(paging, "next"), is("/?page=1&pageSize=2")); 138 | assertThat(isAbsent(paging, "prev"), is(true)); 139 | assertThat(isAbsent(paging, "last"), is(true)); 140 | } 141 | 142 | @Test 143 | public void shouldBuildUrisForMiddlePage() { 144 | Links paging = zeroBasedNumberedPaging(3, 2, true).links(URI_TEMPLATE, ALL_RELS); 145 | 146 | assertThat(hrefFrom(paging, "self"), is("/?page=3&pageSize=2")); 147 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=2")); 148 | assertThat(hrefFrom(paging, "next"), is("/?page=4&pageSize=2")); 149 | assertThat(hrefFrom(paging, "prev"), is("/?page=2&pageSize=2")); 150 | assertThat(isAbsent(paging, "last"), is(true)); 151 | } 152 | 153 | @Test 154 | public void shouldBuildUrisForLastPage() { 155 | Links paging = zeroBasedNumberedPaging(3, 3, false).links(URI_TEMPLATE, ALL_RELS); 156 | assertThat(hrefFrom(paging, "self"), is("/?page=3&pageSize=3")); 157 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=3")); 158 | assertThat(isAbsent(paging, "next"), is(true)); 159 | assertThat(hrefFrom(paging, "prev"), is("/?page=2&pageSize=3")); 160 | assertThat(isAbsent(paging, "last"), is(true)); 161 | } 162 | 163 | @Test 164 | public void shouldBuildUrisForFirstPageWithKnownTotalCount() { 165 | Links paging = zeroBasedNumberedPaging(0, 3, 10).links(URI_TEMPLATE, ALL_RELS); 166 | 167 | assertThat(hrefFrom(paging, "self"), is("/?page=0&pageSize=3")); 168 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=3")); 169 | assertThat(hrefFrom(paging, "next"), is("/?page=1&pageSize=3")); 170 | assertThat(isAbsent(paging, "prev"), is(true)); 171 | assertThat(hrefFrom(paging, "last"), is("/?page=3&pageSize=3")); 172 | } 173 | 174 | @Test 175 | public void shouldBuildUrisForMiddlePageWithKnownTotalCount() { 176 | Links paging = zeroBasedNumberedPaging(2, 3, 10).links(URI_TEMPLATE, ALL_RELS); 177 | 178 | assertThat(hrefFrom(paging, "self"), is("/?page=2&pageSize=3")); 179 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=3")); 180 | assertThat(hrefFrom(paging, "next"), is("/?page=3&pageSize=3")); 181 | assertThat(hrefFrom(paging, "prev"), is("/?page=1&pageSize=3")); 182 | assertThat(hrefFrom(paging, "last"), is("/?page=3&pageSize=3")); 183 | } 184 | 185 | @Test 186 | public void shouldBuildUrisForLastPageWithKnownTotalCount() { 187 | Links paging = zeroBasedNumberedPaging(4, 3, 10).links(URI_TEMPLATE, ALL_RELS); 188 | 189 | assertThat(hrefFrom(paging, "self"), is("/?page=4&pageSize=3")); 190 | assertThat(hrefFrom(paging, "first"), is("/?page=0&pageSize=3")); 191 | assertThat(isAbsent(paging, "next"), is(true)); 192 | assertThat(hrefFrom(paging, "prev"), is("/?page=3&pageSize=3")); 193 | assertThat(hrefFrom(paging, "last"), is("/?page=3&pageSize=3")); 194 | } 195 | 196 | static class TestNumberedPaging extends NumberedPaging { 197 | 198 | TestNumberedPaging(final int page, final int pageSize, final boolean hasMore) { 199 | super(0, page, pageSize, hasMore); 200 | } 201 | 202 | @Override 203 | protected String pageNumberVar() { 204 | return "p"; 205 | } 206 | 207 | @Override 208 | protected String pageSizeVar() { 209 | return "num"; 210 | } 211 | } 212 | 213 | @Test 214 | public void shouldBeAbleToOverrideTemplateVariables() { 215 | Links paging = new TestNumberedPaging(8, 3, false).links(fromTemplate("/{?p,num}"), of(SELF)); 216 | 217 | assertThat(hrefFrom(paging, "self"), is("/?p=8&num=3")); 218 | } 219 | 220 | private boolean isAbsent(Links links, String rel) { 221 | return !links.getLinkBy(rel).isPresent(); 222 | } 223 | 224 | private String hrefFrom(Links links, String rel) { 225 | return links 226 | .getLinkBy(rel) 227 | .orElseThrow(()->new IllegalStateException(rel + " does not exist!")) 228 | .getHref(); 229 | } 230 | } -------------------------------------------------------------------------------- /example-springboot/src/main/resources/static/browser.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The HAL Browser 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 34 | 35 |
36 | 37 | 46 | 47 | 111 | 112 | 116 | 117 | 121 | 122 | 137 | 138 | 142 | 143 | 164 | 165 | 166 | 196 | 197 | 200 | 201 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 256 | 257 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/paging/OneBasedNumberedPagingTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.paging; 2 | 3 | import com.damnhandy.uri.template.UriTemplate; 4 | import de.otto.edison.hal.Links; 5 | import org.junit.Test; 6 | 7 | import java.util.EnumSet; 8 | 9 | import static com.damnhandy.uri.template.UriTemplate.fromTemplate; 10 | import static de.otto.edison.hal.paging.NumberedPaging.oneBasedNumberedPaging; 11 | import static de.otto.edison.hal.paging.PagingRel.*; 12 | import static java.lang.Integer.MAX_VALUE; 13 | import static java.util.EnumSet.*; 14 | import static org.hamcrest.MatcherAssert.assertThat; 15 | import static org.hamcrest.Matchers.is; 16 | 17 | public class OneBasedNumberedPagingTest { 18 | 19 | public static final UriTemplate URI_TEMPLATE = fromTemplate("/{?page,pageSize}"); 20 | 21 | public static final EnumSet ALL_RELS = allOf(PagingRel.class); 22 | 23 | @Test(expected = IllegalArgumentException.class) 24 | public void shouldFailToSkipNegativePageNum1() { 25 | oneBasedNumberedPaging(0, 2, true); 26 | } 27 | 28 | @Test(expected = IllegalArgumentException.class) 29 | public void shouldFailToSkipNegativePageNum2() { 30 | oneBasedNumberedPaging(0, 2, 42); 31 | } 32 | 33 | @Test(expected = IllegalArgumentException.class) 34 | public void shouldFailToLimitPageSize1() { 35 | oneBasedNumberedPaging(1, 0, true); 36 | } 37 | 38 | @Test(expected = IllegalArgumentException.class) 39 | public void shouldFailToLimitPageSize2() { 40 | oneBasedNumberedPaging(1, 0, 42); 41 | } 42 | 43 | @Test(expected = IllegalArgumentException.class) 44 | public void shouldFailToProvideMoreElements() { 45 | oneBasedNumberedPaging(1, Integer.MAX_VALUE, true); 46 | } 47 | 48 | @Test(expected = IllegalArgumentException.class) 49 | public void shouldFailToHaveTotalCountLessThenZero() { 50 | oneBasedNumberedPaging(1, 4, -1); 51 | } 52 | 53 | @Test 54 | public void shouldHandleEmptyPage() { 55 | final NumberedPaging p = oneBasedNumberedPaging(1, 100, 0); 56 | 57 | assertThat(p.getPageNumber(), is(1)); 58 | assertThat(p.getPageSize(), is(100)); 59 | assertThat(p.getTotal().getAsInt(), is(0)); 60 | assertThat(p.getLastPage().getAsInt(), is(1)); 61 | } 62 | 63 | @Test 64 | public void shouldNotHaveLastPage() { 65 | final NumberedPaging p = oneBasedNumberedPaging(1, 100, true); 66 | 67 | assertThat(p.getLastPage().isPresent(), is(false)); 68 | } 69 | 70 | @Test 71 | public void shouldNotMorePages() { 72 | final NumberedPaging p = oneBasedNumberedPaging(1, 100, true); 73 | 74 | assertThat(p.hasMore(), is(true)); 75 | } 76 | 77 | @Test 78 | public void shouldHandleMostlyEmptySinglePage() { 79 | final NumberedPaging p = oneBasedNumberedPaging(1, 100, 1); 80 | 81 | assertThat(p.getPageNumber(), is(1)); 82 | assertThat(p.getPageSize(), is(100)); 83 | assertThat(p.getTotal().getAsInt(), is(1)); 84 | assertThat(p.getLastPage().getAsInt(), is(1)); 85 | assertThat(p.hasMore(), is(false)); 86 | } 87 | 88 | @Test 89 | public void shouldHandleFullSinglePage() { 90 | final NumberedPaging p = oneBasedNumberedPaging(1, 100, 100); 91 | 92 | assertThat(p.getPageNumber(), is(1)); 93 | assertThat(p.getPageSize(), is(100)); 94 | assertThat(p.getTotal().getAsInt(), is(100)); 95 | assertThat(p.getLastPage().getAsInt(), is(1)); 96 | assertThat(p.hasMore(), is(false)); 97 | } 98 | 99 | @Test 100 | public void shouldHandleMultiplePages() { 101 | final NumberedPaging p = oneBasedNumberedPaging(1, 100, 201); 102 | 103 | assertThat(p.getPageNumber(), is(1)); 104 | assertThat(p.getPageSize(), is(100)); 105 | assertThat(p.getTotal().getAsInt(), is(201)); 106 | assertThat(p.getLastPage().getAsInt(), is(3)); 107 | assertThat(p.hasMore(), is(true)); 108 | } 109 | 110 | @Test 111 | public void shouldBuildLinksForEmptyPage() { 112 | Links paging = oneBasedNumberedPaging(1, 100, 0).links(URI_TEMPLATE, ALL_RELS); 113 | 114 | assertThat(hrefFrom(paging, "self"), is("/?page=1&pageSize=100")); 115 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=100")); 116 | assertThat(paging.getLinkBy("next").isPresent(), is(false)); 117 | assertThat(paging.getLinkBy("prev").isPresent(), is(false)); 118 | assertThat(hrefFrom(paging, "last"), is("/?page=1&pageSize=100")); 119 | } 120 | 121 | @Test 122 | public void shouldOnlyBuildWantedLinks() { 123 | Links paging = oneBasedNumberedPaging(2, 3, 10).links(URI_TEMPLATE, range(PREV, NEXT)); 124 | 125 | assertThat(isAbsent(paging, "self"), is(true)); 126 | assertThat(isAbsent(paging, "first"), is(true)); 127 | assertThat(isAbsent(paging, "next"), is(false)); 128 | assertThat(isAbsent(paging, "prev"), is(false)); 129 | assertThat(isAbsent(paging, "last"), is(true)); 130 | } 131 | 132 | 133 | @Test 134 | public void shouldBuildUriWithoutParams() { 135 | Links paging = oneBasedNumberedPaging(1, MAX_VALUE, false).links(URI_TEMPLATE, ALL_RELS); 136 | 137 | assertThat(hrefFrom(paging, "self"), is("/")); 138 | assertThat(hrefFrom(paging, "first"), is("/")); 139 | assertThat(paging.getLinkBy("next").isPresent(), is(false)); 140 | assertThat(paging.getLinkBy("prev").isPresent(), is(false)); 141 | assertThat(paging.getLinkBy("last").isPresent(), is(false)); 142 | } 143 | 144 | @Test 145 | public void shouldBuildUrisForFirstPage() { 146 | Links paging = oneBasedNumberedPaging(1, 2, true).links(URI_TEMPLATE, ALL_RELS); 147 | 148 | assertThat(hrefFrom(paging, "self"), is("/?page=1&pageSize=2")); 149 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=2")); 150 | assertThat(hrefFrom(paging, "next"), is("/?page=2&pageSize=2")); 151 | assertThat(isAbsent(paging, "prev"), is(true)); 152 | assertThat(isAbsent(paging, "last"), is(true)); 153 | } 154 | 155 | @Test 156 | public void shouldBuildUrisForMiddlePage() { 157 | Links paging = oneBasedNumberedPaging(4, 2, true).links(URI_TEMPLATE, ALL_RELS); 158 | 159 | assertThat(hrefFrom(paging, "self"), is("/?page=4&pageSize=2")); 160 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=2")); 161 | assertThat(hrefFrom(paging, "next"), is("/?page=5&pageSize=2")); 162 | assertThat(hrefFrom(paging, "prev"), is("/?page=3&pageSize=2")); 163 | assertThat(isAbsent(paging, "last"), is(true)); 164 | } 165 | 166 | @Test 167 | public void shouldBuildNotBuildUriForLastPage() { 168 | Links paging = oneBasedNumberedPaging(1, 3, true).links(URI_TEMPLATE, of(LAST)); 169 | assertThat(isAbsent(paging, "last"), is(true)); 170 | } 171 | 172 | @Test 173 | public void shouldBuildUrisForLastPage() { 174 | Links paging = oneBasedNumberedPaging(4, 3, false).links(URI_TEMPLATE, ALL_RELS); 175 | assertThat(hrefFrom(paging, "self"), is("/?page=4&pageSize=3")); 176 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=3")); 177 | assertThat(isAbsent(paging, "next"), is(true)); 178 | assertThat(hrefFrom(paging, "prev"), is("/?page=3&pageSize=3")); 179 | assertThat(isAbsent(paging, "last"), is(true)); 180 | } 181 | 182 | @Test 183 | public void shouldBuildUrisForFirstPageWithKnownTotalCount() { 184 | Links paging = oneBasedNumberedPaging(1, 3, 10).links(URI_TEMPLATE, ALL_RELS); 185 | 186 | assertThat(hrefFrom(paging, "self"), is("/?page=1&pageSize=3")); 187 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=3")); 188 | assertThat(hrefFrom(paging, "next"), is("/?page=2&pageSize=3")); 189 | assertThat(isAbsent(paging, "prev"), is(true)); 190 | assertThat(hrefFrom(paging, "last"), is("/?page=4&pageSize=3")); 191 | } 192 | 193 | @Test 194 | public void shouldBuildUrisForMiddlePageWithKnownTotalCount() { 195 | Links paging = oneBasedNumberedPaging(3, 3, 10).links(URI_TEMPLATE, ALL_RELS); 196 | 197 | assertThat(hrefFrom(paging, "self"), is("/?page=3&pageSize=3")); 198 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=3")); 199 | assertThat(hrefFrom(paging, "next"), is("/?page=4&pageSize=3")); 200 | assertThat(hrefFrom(paging, "prev"), is("/?page=2&pageSize=3")); 201 | assertThat(hrefFrom(paging, "last"), is("/?page=4&pageSize=3")); 202 | } 203 | 204 | @Test 205 | public void shouldBuildUrisForLastPageWithKnownTotalCount() { 206 | Links paging = oneBasedNumberedPaging(5, 3, 10).links(URI_TEMPLATE, ALL_RELS); 207 | 208 | assertThat(hrefFrom(paging, "self"), is("/?page=5&pageSize=3")); 209 | assertThat(hrefFrom(paging, "first"), is("/?page=1&pageSize=3")); 210 | assertThat(isAbsent(paging, "next"), is(true)); 211 | assertThat(hrefFrom(paging, "prev"), is("/?page=4&pageSize=3")); 212 | assertThat(hrefFrom(paging, "last"), is("/?page=4&pageSize=3")); 213 | } 214 | 215 | static class TestNumberedPaging extends NumberedPaging { 216 | 217 | TestNumberedPaging(final int page, final int pageSize, final boolean hasMore) { 218 | super(1, page, pageSize, hasMore); 219 | } 220 | 221 | @Override 222 | protected String pageNumberVar() { 223 | return "p"; 224 | } 225 | 226 | @Override 227 | protected String pageSizeVar() { 228 | return "num"; 229 | } 230 | } 231 | 232 | @Test 233 | public void shouldBeAbleToOverrideTemplateVariables() { 234 | Links paging = new TestNumberedPaging(8, 3, false).links(fromTemplate("/{?p,num}"), of(SELF)); 235 | 236 | assertThat(hrefFrom(paging, "self"), is("/?p=8&num=3")); 237 | } 238 | 239 | private boolean isAbsent(Links links, String rel) { 240 | return !links.getLinkBy(rel).isPresent(); 241 | } 242 | 243 | private String hrefFrom(Links links, String rel) { 244 | return links 245 | .getLinkBy(rel) 246 | .orElseThrow(()->new IllegalStateException(rel + " does not exist!")) 247 | .getHref(); 248 | } 249 | } -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/paging/SkipLimitPagingTest.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal.paging; 2 | 3 | import com.damnhandy.uri.template.UriTemplate; 4 | import de.otto.edison.hal.Links; 5 | import org.junit.Assert; 6 | import org.junit.Rule; 7 | import org.junit.Test; 8 | import org.junit.rules.ExpectedException; 9 | 10 | import java.util.EnumSet; 11 | import java.util.OptionalInt; 12 | 13 | import static com.damnhandy.uri.template.UriTemplate.fromTemplate; 14 | import static de.otto.edison.hal.Links.linkingTo; 15 | import static de.otto.edison.hal.paging.PagingRel.*; 16 | import static de.otto.edison.hal.paging.SkipLimitPaging.skipLimitPage; 17 | import static java.lang.Integer.MAX_VALUE; 18 | import static java.util.EnumSet.*; 19 | import static java.util.OptionalInt.empty; 20 | import static org.hamcrest.MatcherAssert.assertThat; 21 | import static org.hamcrest.Matchers.is; 22 | 23 | public class SkipLimitPagingTest { 24 | 25 | public static final UriTemplate URI_TEMPLATE = fromTemplate("/{?skip,limit}"); 26 | 27 | public static final EnumSet ALL_RELS = allOf(PagingRel.class); 28 | 29 | @Test(expected = IllegalArgumentException.class) 30 | public void shouldFailToSkipNegativeNumElements1() { 31 | skipLimitPage(-1, 2, true); 32 | } 33 | 34 | @Test(expected = IllegalArgumentException.class) 35 | public void shouldFailToSkipNegativeNumElements2() { 36 | skipLimitPage(-1, 2, 42); 37 | } 38 | 39 | @Test(expected = IllegalArgumentException.class) 40 | public void shouldFailToLimitNegativeNumElements1() { 41 | skipLimitPage(0, -1, true); 42 | } 43 | 44 | @Test(expected = IllegalArgumentException.class) 45 | public void shouldFailToLimitNegativeNumElements2() { 46 | skipLimitPage(0, -1, 42); 47 | } 48 | 49 | @Test(expected = IllegalArgumentException.class) 50 | public void shouldFailToProvideMoreElements() { 51 | skipLimitPage(0, Integer.MAX_VALUE, true); 52 | } 53 | 54 | @Test(expected = IllegalArgumentException.class) 55 | public void shouldFailToHaveTotalCountLessThenZero() { 56 | skipLimitPage(0, 4, -1); 57 | } 58 | 59 | @Test(expected = IllegalArgumentException.class) 60 | public void shouldFailToSkipBehindLastPage() { 61 | skipLimitPage(6, 4, 5); 62 | } 63 | 64 | @Test 65 | public void shouldInitHasMoreFromTotal() { 66 | final SkipLimitPaging paging = skipLimitPage(1, 2, 4); 67 | assertThat(paging.getLimit(), is(2)); 68 | assertThat(paging.getSkip(), is(1)); 69 | assertThat(paging.getTotal(), is(OptionalInt.of(4))); 70 | assertThat(paging.hasMore(), is(true)); 71 | } 72 | 73 | @Test 74 | public void shouldInitTotalAsEmpty() { 75 | final SkipLimitPaging paging = skipLimitPage(1, 2, true); 76 | assertThat(paging.getLimit(), is(2)); 77 | assertThat(paging.getSkip(), is(1)); 78 | assertThat(paging.getTotal(), is(empty())); 79 | assertThat(paging.hasMore(), is(true)); 80 | } 81 | 82 | @Test 83 | public void shouldCreateLinksForEmptyPage() { 84 | Links paging = skipLimitPage(0, 3, 0).links(URI_TEMPLATE, ALL_RELS); 85 | assertThat(hrefFrom(paging, "self"), is("/?skip=0&limit=3")); 86 | assertThat(isAbsent(paging, "prev"), is(true)); 87 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 88 | assertThat(isAbsent(paging, "next"), is(true)); 89 | assertThat(hrefFrom(paging, "last"), is("/?skip=0&limit=3")); 90 | } 91 | 92 | @Test(expected = IllegalArgumentException.class) 93 | public void shouldNotPageAfterLastPage() { 94 | skipLimitPage(1, 3, 0); 95 | } 96 | 97 | @Test 98 | public void shouldOnlyBuildWantedLinks() { 99 | Links paging = linkingTo().with(skipLimitPage(3, 3, 10).links(URI_TEMPLATE, range(PREV, NEXT))).build(); 100 | 101 | assertThat(isAbsent(paging, "self"), is(true)); 102 | assertThat(isAbsent(paging, "first"), is(true)); 103 | assertThat(isAbsent(paging, "next"), is(false)); 104 | assertThat(isAbsent(paging, "prev"), is(false)); 105 | assertThat(isAbsent(paging, "last"), is(true)); 106 | } 107 | 108 | 109 | @Test 110 | public void shouldBuildUriWithoutParams() { 111 | Links paging = skipLimitPage(0, MAX_VALUE, false).links(URI_TEMPLATE, ALL_RELS); 112 | 113 | assertThat(hrefFrom(paging, "self"), is("/")); 114 | assertThat(hrefFrom(paging, "first"), is("/")); 115 | assertThat(paging.getLinkBy("next").isPresent(), is(false)); 116 | assertThat(paging.getLinkBy("prev").isPresent(), is(false)); 117 | assertThat(paging.getLinkBy("last").isPresent(), is(false)); 118 | } 119 | 120 | @Test 121 | public void shouldBuildUrisForFirstPage() { 122 | Links paging = skipLimitPage(0, 2, true).links(URI_TEMPLATE, ALL_RELS); 123 | 124 | assertThat(hrefFrom(paging, "self"), is("/?skip=0&limit=2")); 125 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=2")); 126 | assertThat(hrefFrom(paging, "next"), is("/?skip=2&limit=2")); 127 | assertThat(isAbsent(paging, "prev"), is(true)); 128 | assertThat(isAbsent(paging, "last"), is(true)); 129 | } 130 | 131 | @Test 132 | public void shouldBuildUrisForMiddlePage() { 133 | Links paging = skipLimitPage(1, 2, true).links(URI_TEMPLATE, ALL_RELS); 134 | 135 | assertThat(hrefFrom(paging, "self"), is("/?skip=1&limit=2")); 136 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=2")); 137 | assertThat(hrefFrom(paging, "next"), is("/?skip=3&limit=2")); 138 | assertThat(hrefFrom(paging, "prev"), is("/?skip=0&limit=2")); 139 | assertThat(isAbsent(paging, "last"), is(true)); 140 | } 141 | 142 | @Test 143 | public void shouldBuildUrisForLastPage() { 144 | Links paging = skipLimitPage(6, 3, false).links(URI_TEMPLATE, ALL_RELS); 145 | assertThat(hrefFrom(paging, "self"), is("/?skip=6&limit=3")); 146 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 147 | assertThat(isAbsent(paging, "next"), is(true)); 148 | assertThat(hrefFrom(paging, "prev"), is("/?skip=3&limit=3")); 149 | assertThat(isAbsent(paging, "last"), is(true)); 150 | } 151 | 152 | @Test 153 | public void shouldBuildUrisForFirstPageWithKnownTotalCount() { 154 | Links paging = skipLimitPage(0, 3, 10).links(URI_TEMPLATE, ALL_RELS); 155 | 156 | assertThat(hrefFrom(paging, "self"), is("/?skip=0&limit=3")); 157 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 158 | assertThat(hrefFrom(paging, "next"), is("/?skip=3&limit=3")); 159 | assertThat(isAbsent(paging, "prev"), is(true)); 160 | assertThat(hrefFrom(paging, "last"), is("/?skip=9&limit=3")); 161 | } 162 | 163 | @Test 164 | public void shouldBuildUrisForLagePageWithTotalAsMultipleOfSkip() { 165 | Links paging = skipLimitPage(0, 5, 10).links(URI_TEMPLATE, of(LAST)); 166 | 167 | assertThat(hrefFrom(paging, "last"), is("/?skip=5&limit=5")); 168 | } 169 | 170 | @Test 171 | public void shouldBuildUrisForMiddlePageWithKnownTotalCount() { 172 | Links paging = skipLimitPage(5, 3, 10).links(URI_TEMPLATE, ALL_RELS); 173 | 174 | assertThat(hrefFrom(paging, "self"), is("/?skip=5&limit=3")); 175 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 176 | assertThat(hrefFrom(paging, "next"), is("/?skip=8&limit=3")); 177 | assertThat(hrefFrom(paging, "prev"), is("/?skip=2&limit=3")); 178 | assertThat(hrefFrom(paging, "last"), is("/?skip=9&limit=3")); 179 | } 180 | 181 | @Test 182 | public void shouldBuildUrisForMiddlePageWithKnownTotalCount2() { 183 | Links paging = skipLimitPage(4, 3, 10).links(URI_TEMPLATE, ALL_RELS); 184 | 185 | assertThat(hrefFrom(paging, "self"), is("/?skip=4&limit=3")); 186 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 187 | assertThat(hrefFrom(paging, "next"), is("/?skip=7&limit=3")); 188 | assertThat(hrefFrom(paging, "prev"), is("/?skip=1&limit=3")); 189 | assertThat(hrefFrom(paging, "last"), is("/?skip=9&limit=3")); 190 | } 191 | 192 | @Test 193 | public void shouldBuildUrisForMiddlePageWithKnownTotalCount3() { 194 | Links paging = skipLimitPage(3, 3, 10).links(URI_TEMPLATE, ALL_RELS); 195 | 196 | assertThat(hrefFrom(paging, "self"), is("/?skip=3&limit=3")); 197 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 198 | assertThat(hrefFrom(paging, "next"), is("/?skip=6&limit=3")); 199 | assertThat(hrefFrom(paging, "prev"), is("/?skip=0&limit=3")); 200 | assertThat(hrefFrom(paging, "last"), is("/?skip=9&limit=3")); 201 | } 202 | 203 | @Test 204 | public void shouldBuildUrisForLastPageWithKnownTotalCount() { 205 | Links paging = skipLimitPage(8, 3, 10).links(URI_TEMPLATE, ALL_RELS); 206 | 207 | assertThat(hrefFrom(paging, "self"), is("/?skip=8&limit=3")); 208 | assertThat(hrefFrom(paging, "first"), is("/?skip=0&limit=3")); 209 | assertThat(isAbsent(paging, "next"), is(true)); 210 | assertThat(hrefFrom(paging, "prev"), is("/?skip=5&limit=3")); 211 | assertThat(hrefFrom(paging, "last"), is("/?skip=8&limit=3")); 212 | } 213 | 214 | static class TestSkipLimitPaging extends SkipLimitPaging { 215 | 216 | TestSkipLimitPaging(final int skip, final int limit, final boolean hasMore) { 217 | super(skip, limit, hasMore); 218 | } 219 | 220 | @Override 221 | protected String skipVar() { 222 | return "s"; 223 | } 224 | 225 | @Override 226 | protected String limitVar() { 227 | return "num"; 228 | } 229 | } 230 | 231 | @Test 232 | public void shouldBeAbleToOverrideTemplateVariables() { 233 | Links paging = new TestSkipLimitPaging(8, 3, false).links(fromTemplate("/{?s,num}"), of(SELF)); 234 | 235 | assertThat(hrefFrom(paging, "self"), is("/?s=8&num=3")); 236 | } 237 | 238 | private boolean isAbsent(Links links, String rel) { 239 | return !links.getLinkBy(rel).isPresent(); 240 | } 241 | 242 | private String hrefFrom(Links links, String rel) { 243 | return links 244 | .getLinkBy(rel) 245 | .orElseThrow(()->new IllegalStateException(rel + " does not exist!")) 246 | .getHref(); 247 | } 248 | } -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 2.2.0 4 | * Dependency updates 5 | * Published via central.sonatype.com instead of oss.sonatype.org 6 | 7 | ## 2.1.1 8 | 9 | * Dependency updates 10 | 11 | ## 2.1.0 12 | 13 | *New Features / API extensions* 14 | 15 | * Adds method `Links.Builder#collection(String)` to add a collection link to a HalResource´s `_links` section. 16 | 17 | *Bugfixes* 18 | 19 | * Fixes issue [#29](https://github.com/otto-de/edison-hal/issues/29): additional attributes contained in 20 | `HalRepresentation.attributes` will now be serialized. 21 | 22 | *Deprecations* 23 | 24 | * Adds deprecation for `HalRepresentation` constructors with `Curies` parameter. No expected usages - but please 25 | contact me, if the removal in some future release 3.0 will break some use-cases for you. 26 | 27 | ## 2.0.2 28 | 29 | *Bugfixes* 30 | 31 | * Fixed issues when more than two parameters in the Traverson.withVars() function. 32 | 33 | ## 2.0.1 34 | 35 | *Bugfixes* 36 | 37 | * Fixes next page bug for zero-based paging (Issue #24) 38 | 39 | ## 2.0.0 40 | 41 | *New Features / API extensions* 42 | 43 | * Issue 23: Allow customization of the Jackson `ObjectMapper` used in `HalParser` `Traverson`. 44 | 45 | *Dependency Updates* 46 | * Updated `com.fasterxml.jackson.core:jackson-databind` from 2.9.1 to 2.9.6 47 | * Updated `com.damnhandy:handy-uri-templates` from 2.1.6 to 2.1.7 48 | 49 | ## 2.0.0-m2 50 | 51 | *Bugfixes* 52 | 53 | * Issue 22: Duplicate curies are now removed from embedded objects, if they are specified in the embedding `HalRepresentation`. 54 | 55 | *Breaking Changes* 56 | 57 | * The API to create Links instances has changed: the former `Links.linkingTo(List)`, 58 | `Links.linkingTo(Link, Link...)` is now replaced by `Links.linkingTo()` which is returning a `Links.Builder`. The 59 | reason for this is that it is now possible to specify more easily, whether a `Link` should be rendered as a single 60 | link object, or an array of link objects. The `Links.Builder` has now methods for this like, for example, 61 | `single(Link, Link...)` or `array(Link, Link...)`. Have a look at 62 | [section 4.3](#4.3-single-link-objects-vs.-arrays-of-link-objects) for more details about adding links to a 63 | `HalRepresentation`. 64 | * Because the `Links.Builder` is now able to create single link objects as well as arrays of link objects, the 65 | corresponding functionality to register link-relation types to be rendered as arrays has been removed from 66 | `RelRegistry`. 67 | * Issue 21: Similar to the links, it is now possible to specify whether or not embedded resources are embedded as 68 | single resource objects, or arrays of resources objects. 69 | * Renamed `RelRegistry` to `Curies` 70 | * Issue 20: HalRepresentation was previously annotated with @JsonInclude(NON_NULL). This was changed so that only 71 | _links and _embedded are now annotated this way. This might change the behaviour / structure of existing applications. 72 | You should now annotate classes extending HalRepresentation, or attributes of such classes appropriately. 73 | 74 | ## 2.0.0-m1 75 | 76 | *Breaking Changes* 77 | 78 | * The error handling in the Traverson API has been changed in that exceptions are now thrown instead of catching them 79 | and exposing a `Traverson.getLastError()`. In 1.0.0, the client had to check for the last error. This is rather unusual 80 | and is easy to overlook. Beginning with 2.0.0, `getLastError` is removed and the following method-signatures are now 81 | throwing `java.io.IOException`: 82 | * Traverson.paginateNext() 83 | * Traverson.paginateNextAs() 84 | * Traverson.paginatePrev() 85 | * Traverson.paginatePrevAs() 86 | * Traverson.stream() 87 | * Traverson.streamAs() 88 | * Traverson.getResource() 89 | * Traverson.getResourceAs() 90 | * The static factory method `Traverson.traverson()` does not accept a `java.util.function.Function` 91 | anymore. Instead, a `de.otto.edison.hal.traverson.LinkResolver` was introduced. The major difference between the new 92 | `PageHandler` and the previous `Function` is that the `PageHandler.apply(Link)` is now throwing `IOException` while 93 | `Function` does not allow this. 94 | * For the same reasons (being able to throw IOExceptions), the different `paginate*` methods now expect a 95 | `de.otto.edison.hal.traverson.PageHandler` instead of a `java.util.function.Function`. Beside of this, 96 | using dedicated functional interfaces instead of generic Functions is a little easier to understand. 97 | 98 | 99 | *New Features / API extensions* 100 | 101 | * The `Traverson` now supports traversing linked resources while ignoring embedded resources: instead of returning an 102 | embedded item, clients are now able to force the `Traverson` to follow the links instead. This is especially helpful, 103 | if only a reduced set of attributes is embedded into a resource. A set of `Traverson.followLink()` methods was added 104 | to support this. 105 | * Added new methods `Traverson.paginate()` and `Traverson.paginateAs()` to paginate over paged resources using 106 | link-relation types other than `next` or `prev`. 107 | * Added a `CuriTemplate` helper to expand / shorten link-relation types using a CURI. 108 | 109 | ## 1.0.0 110 | 111 | *New Features / API extensions* 112 | * It is now possible to configure the link-relation types that are serialized as an array of links. 113 | * Parsing of nested embedded items 114 | * Support for curies in deeply nested embedded items 115 | * The HalParser now supports multiple type infos so more than one link-relation type can 116 | be configured with the type of the embedded items. 117 | * Support for parsing and accessing attributes that were not mapped to properties of HalRepresentations 118 | * Added TRACE logging to Traverson to make it easier to analyse the behaviour of the Traverson. 119 | 120 | ## 1.0.0.RC5 121 | 122 | *Bugfixes* 123 | 124 | * Fixed signature of HalRepresentation.withEmbedded(): using List, EmbeddedTypeInfo)` and `Traverson.streamAs(Class, EmbeddedTypeInfo> 138 | so it is possible to specify the type of embedded items of a resource using Traversons. 139 | * Added support for client-side traversal of paged resources using 140 | - `Traverson.paginateNext()` 141 | - `Traverson.paginateNextAs()` 142 | - `Traverson.paginatePrev()` 143 | - `Traverson.paginatePrevAs()` 144 | - `Traverson.paginate()` 145 | - `Traverson.paginateAs()` 146 | 147 | ## 1.0.0.RC2 148 | 149 | *Bugfixes* 150 | 151 | * Fixed traversion of links using predicates 152 | * Fixed parsing of embedded items, where a rel has only only a single item instead of a list of items. 153 | * Fixed getter for SkipLimitPaging.hasMore 154 | 155 | ## 1.0.0.RC1 156 | 157 | *New Features / API extensions* 158 | 159 | * New Traverson methods to select links matching some given predicate. 160 | 161 | ## 0.7.0 162 | 163 | *Breaking Changes* 164 | 165 | * Deprecated NumberedPaging.numberedPaging(). 166 | 167 | *New Features / API extensions* 168 | 169 | * Introduced support for 1-based paging. 170 | * New builder methods NumberedPaging.zeroBasedNumberedPaging() and 171 | NumberedPaging.oneBasedNumberedPaging() 172 | 173 | ## 0.6.2 174 | 175 | *Bugfixes* 176 | 177 | * The constructors of NumberedPaging are now protected instead of final. 178 | This prevented changing the names of the page/pageSize variables used 179 | in collection resources. 180 | * Fixed numbering of last-page links in NumberedPaging. 181 | 182 | *New Features / API extensions* 183 | 184 | * Added NumberedPaging.getLastPage() 185 | 186 | ## 0.6.1 187 | 188 | *Bugfixes* 189 | 190 | * Fixed a bug that prevented the use of paging for empty collections. 191 | 192 | ## 0.6.0 193 | 194 | *Breaking Changes* 195 | 196 | * Moved Traverson classes to package de.otto.edison.hal.traverson 197 | 198 | *Bugfixes* 199 | 200 | * Fixed shortening of embedded links using curies when adding links to 201 | a HalResource after construction. 202 | 203 | *New Features / API extensions* 204 | 205 | * Added Link.getHrefAsTemplate() 206 | * Added helpers to create links for paged resources: NumberedPaging and SkipLimitPaging 207 | 208 | ## 0.5.0 209 | 210 | *Breaking Changes* 211 | 212 | * Renamed Link.Builder.fromPrototype() and Links.Builder.fromPrototype() 213 | to copyOf() 214 | 215 | *New Features / API extensions* 216 | 217 | * Added Link.isEquivalentTo(Link) 218 | * Link.Builder is not adding equivalent links anymore 219 | * Added HalRepresentation.withEmbedded() and HalRepresentation.withLinks() 220 | so links and embedded items can be added after construction. 221 | 222 | ## 0.4.1 223 | 224 | *New Features / API extensions* 225 | 226 | * Added Traverson.startWith(HalRepresentation) to initialize a Traverson from a given resource. 227 | 228 | *Bufixes* 229 | 230 | * JsonSerializers and -Deserializers for Links and Embedded are now public to avoid problems with some testing szenarios in Spring. 231 | 232 | ## 0.4.0 233 | 234 | *Breaking Changes* 235 | 236 | * Simplified creation of links by removing unneeded factory methods for 237 | templated links. Whether or not a link is templated is now automatically 238 | identified by the Link. 239 | * Removed duplicate factory method to create a Link.Builder. 240 | 241 | *New Features / API extensions* 242 | 243 | * Added a Traverson API to navigate through HAL resources. 244 | 245 | 246 | ## 0.3.0 247 | 248 | *Bugfixes* 249 | 250 | * curies are now rendered as an array of links instead of a single link 251 | document 252 | 253 | *New Features / API extensions* 254 | 255 | * Added factory method Link.curi() to build CURI links. 256 | * Support for curies in links and embedded resources. 257 | * Improved JavaDoc 258 | * Added Links.getRels() 259 | * Added Links.stream() 260 | * Added Embedded.getRels() 261 | * Added simple example for a client of a HAL service. 262 | 263 | ## 0.2.0 264 | 265 | *Bugfixes* 266 | 267 | * Fixed generation + parsing of non-trivial links 268 | * Fixed type and name of 'deprecation' property in links 269 | * Fixed rendering of empty embedded items 270 | * Fixed rendering of empty links 271 | 272 | *Breaking Changes* 273 | 274 | * Renamed Link.LinkBuilder to Link.Builder 275 | * Renamed Embedded.EmbeddedItemsBuilder to Embedded.Builder 276 | * Renamed Embedded.Builder.withEmbedded() to Embedded.Builder.with() 277 | * Renamed Embedded.Builder.withoutEmbedded() to Embedded.Builder.without() 278 | * Added getter methods to Link instead of public final attributes 279 | 280 | *New Features / API extensions* 281 | 282 | * Introduced factory methods for Embedded.Builder 283 | * Improved JavaDoc 284 | * Added Spring-Boot example aplication incl HAL Browser 285 | * Added Links.linkingTo(List links) 286 | * Added Links.Builder 287 | * Added Embedded.isEmpty() 288 | 289 | ## 0.1.0 290 | 291 | * Initial Release 292 | * Full support for all link properties specified by https://tools.ietf.org/html/draft-kelly-json-hal-08 293 | * Full support for embedded resources. 294 | * Serialization and deserialization of HAL resources. 295 | 296 | -------------------------------------------------------------------------------- /src/test/java/de/otto/edison/hal/UserGuideExamples.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.junit.Test; 7 | 8 | import java.io.IOException; 9 | import java.util.List; 10 | 11 | import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT; 12 | import static de.otto.edison.hal.Embedded.embedded; 13 | import static de.otto.edison.hal.Embedded.embeddedBuilder; 14 | import static de.otto.edison.hal.EmbeddedTypeInfo.withEmbedded; 15 | import static de.otto.edison.hal.Link.link; 16 | import static de.otto.edison.hal.Link.self; 17 | import static de.otto.edison.hal.Links.linkingTo; 18 | import static java.util.Arrays.asList; 19 | import static java.util.Collections.singletonList; 20 | import static org.hamcrest.CoreMatchers.equalTo; 21 | import static org.hamcrest.MatcherAssert.assertThat; 22 | import static org.hamcrest.Matchers.hasSize; 23 | import static org.hamcrest.core.Is.is; 24 | 25 | /** 26 | * Simple Tests to prove that the examples used in the README.md are compiling and working 27 | */ 28 | public class UserGuideExamples { 29 | 30 | private static final ObjectMapper objectMapper; 31 | private static final ObjectMapper prettyObjectMapper; 32 | static { 33 | objectMapper = new ObjectMapper(); 34 | prettyObjectMapper = new ObjectMapper(); 35 | prettyObjectMapper.enable(INDENT_OUTPUT); 36 | } 37 | 38 | @Test 39 | public void Example_1_1() { 40 | // snippet 41 | final HalRepresentation representation = new HalRepresentation( 42 | linkingTo() 43 | .self("http://example.org/test/bar") 44 | .item("http://example.org/test/foo/01") 45 | .item("http://example.org/test/foo/02") 46 | .build(), 47 | embeddedBuilder() 48 | .with("item", asList( 49 | new HalRepresentation(linkingTo().self("http://example.org/test/foo/01").build()), 50 | new HalRepresentation(linkingTo().self("http://example.org/test/foo/02").build()))) 51 | .build()); 52 | // /snippet 53 | assertThat(jsonOf("Example_1_1", representation), is("{\"_links\":{\"self\":{\"href\":\"http://example.org/test/bar\"},\"item\":[{\"href\":\"http://example.org/test/foo/01\"},{\"href\":\"http://example.org/test/foo/02\"}]},\"_embedded\":{\"item\":[{\"_links\":{\"self\":{\"href\":\"http://example.org/test/foo/01\"}}},{\"_links\":{\"self\":{\"href\":\"http://example.org/test/foo/02\"}}}]}}")); 54 | } 55 | 56 | @Test 57 | public void Example_1_2() { 58 | // snippet 59 | class Example_1_2 extends HalRepresentation { 60 | @JsonProperty("someProperty") 61 | private String someProperty = "some value"; 62 | @JsonProperty("someOtherProperty") 63 | private String someOtherProperty = "some other value"; 64 | 65 | Example_1_2() { 66 | super(linkingTo() 67 | .self("http://example.org/test/bar") 68 | .build() 69 | ); 70 | } 71 | } 72 | // /snippet 73 | Example_1_2 representation = new Example_1_2(); 74 | assertThat(jsonOf("Example_1_2", representation), is("{\"_links\":{\"self\":{\"href\":\"http://example.org/test/bar\"}},\"someProperty\":\"some value\",\"someOtherProperty\":\"some other value\"}")); 75 | } 76 | 77 | @Test 78 | public void Example_2_1() { 79 | // snippet 80 | final HalRepresentation representation = new HalRepresentation( 81 | linkingTo() 82 | .self("http://example.com/foo/42") 83 | .single(link("next", "http://example.com/foo/43")) 84 | .single(link("prev", "http://example.com/foo/41")) 85 | .array( 86 | link("item", "http://example.com/bar/01"), 87 | link("item", "http://example.com/bar/02"), 88 | link("item", "http://example.com/bar/03") 89 | ) 90 | .build() 91 | ); 92 | // /snippet 93 | assertThat(jsonOf("Example_2_1", representation), is("{\"_links\":{\"self\":{\"href\":\"http://example.com/foo/42\"},\"next\":{\"href\":\"http://example.com/foo/43\"},\"prev\":{\"href\":\"http://example.com/foo/41\"},\"item\":[{\"href\":\"http://example.com/bar/01\"},{\"href\":\"http://example.com/bar/02\"},{\"href\":\"http://example.com/bar/03\"}]}}")); 94 | } 95 | 96 | @Test 97 | public void Example_2_2() { 98 | // snippet 99 | final HalRepresentation representation = new HalRepresentation( 100 | linkingTo() 101 | .curi("x", "http://example.org/rels/{rel}") 102 | .curi("y", "http://example.com/rels/{rel}") 103 | .single( 104 | link("http://example.org/rels/foo", "http://example.org/test")) 105 | .array( 106 | link("http://example.com/rels/bar", "http://example.org/test/1"), 107 | link("http://example.com/rels/bar", "http://example.org/test/2")) 108 | .build() 109 | ); 110 | // /snippet 111 | assertThat(jsonOf("Example_2_2", representation), is("{\"_links\":{\"curies\":[{\"href\":\"http://example.org/rels/{rel}\",\"templated\":true,\"name\":\"x\"},{\"href\":\"http://example.com/rels/{rel}\",\"templated\":true,\"name\":\"y\"}],\"x:foo\":{\"href\":\"http://example.org/test\"},\"y:bar\":[{\"href\":\"http://example.org/test/1\"},{\"href\":\"http://example.org/test/2\"}]}}")); 112 | } 113 | 114 | @Test 115 | public void Example_4_1() throws IOException { 116 | 117 | // given 118 | final String json = 119 | "{" + 120 | "\"someProperty\":\"1\"," + 121 | "\"someOtherProperty\":\"2\"," + 122 | "\"_links\":{\"self\":{\"href\":\"http://example.org/test/foo\"}}," + 123 | "\"_embedded\":{\"bar\":[" + 124 | "{" + 125 | "\"_links\":{\"self\":{\"href\":\"http://example.org/test/bar/01\"}}" + 126 | "}" + 127 | "]}" + 128 | "}"; 129 | 130 | // when 131 | final TestHalRepresentation result = new ObjectMapper().readValue(json.getBytes(), TestHalRepresentation.class); 132 | 133 | // then 134 | assertThat(result.someProperty, is("1")); 135 | assertThat(result.someOtherProperty, is("2")); 136 | 137 | // and 138 | final Links links = result.getLinks(); 139 | assertThat(links.getLinkBy("self").get(), is(self("http://example.org/test/foo"))); 140 | 141 | // and 142 | final List embeddedItems = result.getEmbedded().getItemsBy("bar"); 143 | assertThat(embeddedItems, hasSize(1)); 144 | assertThat(embeddedItems.get(0).getLinks().getLinkBy("self").get(), is(link("self", "http://example.org/test/bar/01"))); 145 | } 146 | 147 | @Test 148 | public void Example_4_2() throws IOException { 149 | // given 150 | final String json = 151 | "{" + 152 | " \"_embedded\":{\"bar\":[" + 153 | " {" + 154 | " \"someProperty\":\"3\"," + 155 | " \"someOtherProperty\":\"3\"," + 156 | " \"_links\":{\"self\":[{\"href\":\"http://example.org/test/bar/01\"}]}" + 157 | " }" + 158 | " ]}" + 159 | "}"; 160 | 161 | // when 162 | final HalRepresentation result = HalParser 163 | .parse(json) 164 | .as(HalRepresentation.class, withEmbedded("bar", TestHalRepresentation.class)); 165 | 166 | // then 167 | final List embeddedItems = result 168 | .getEmbedded() 169 | .getItemsBy("bar", TestHalRepresentation.class); 170 | 171 | assertThat(embeddedItems, hasSize(1)); 172 | assertThat(embeddedItems.get(0).getClass(), equalTo(TestHalRepresentation.class)); 173 | assertThat(embeddedItems.get(0).getLinks().getLinkBy("self").get(), is(link("self", "http://example.org/test/bar/01"))); 174 | } 175 | 176 | static class TestHalRepresentation extends HalRepresentation { 177 | @JsonProperty("someProperty") 178 | private String someProperty; 179 | @JsonProperty("someOtherProperty") 180 | private String someOtherProperty; 181 | 182 | TestHalRepresentation() { 183 | super( 184 | linkingTo() 185 | .self("http://example.org/test/foo") 186 | .build(), 187 | embedded("bar", singletonList(new HalRepresentation( 188 | linkingTo() 189 | .self("http://example.org/test/bar/01") 190 | .build() 191 | ))) 192 | ); 193 | } 194 | } 195 | 196 | @Test 197 | public void shouldParseHal() throws IOException { 198 | 199 | // given 200 | final String json = 201 | "{" + 202 | "\"someProperty\":\"1\"," + 203 | "\"someOtherProperty\":\"2\"," + 204 | "\"_links\":{\"self\":{\"href\":\"http://example.org/test/foo\"}}," + 205 | "\"_embedded\":{\"bar\":[" + 206 | "{" + 207 | "\"_links\":{\"self\":{\"href\":\"http://example.org/test/bar/01\"}}" + 208 | "}" + 209 | "]}" + 210 | "}"; 211 | 212 | // when 213 | final TestHalRepresentation result = new ObjectMapper().readValue(json.getBytes(), TestHalRepresentation.class); 214 | 215 | // then 216 | assertThat(result.someProperty, is("1")); 217 | assertThat(result.someOtherProperty, is("2")); 218 | 219 | // and 220 | final Links links = result.getLinks(); 221 | assertThat(links.getLinkBy("self").get(), is(self("http://example.org/test/foo"))); 222 | 223 | // and 224 | final List embeddedItems = result.getEmbedded().getItemsBy("bar"); 225 | assertThat(embeddedItems, hasSize(1)); 226 | assertThat(embeddedItems.get(0).getLinks().getLinkBy("self").get(), is(link("self", "http://example.org/test/bar/01"))); 227 | } 228 | 229 | private String jsonOf(String method, HalRepresentation representation) { 230 | try { 231 | System.out.println(method + ":"); 232 | System.out.println(prettyObjectMapper.writeValueAsString(representation)); 233 | return objectMapper.writeValueAsString(representation); 234 | } catch (final JsonProcessingException e) { 235 | throw new IllegalArgumentException(e.getMessage(), e); 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/de/otto/edison/hal/HalRepresentation.java: -------------------------------------------------------------------------------- 1 | package de.otto.edison.hal; 2 | 3 | import com.fasterxml.jackson.annotation.*; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | 6 | import java.util.ArrayList; 7 | import java.util.LinkedHashMap; 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | import static com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL; 12 | import static de.otto.edison.hal.Curies.emptyCuries; 13 | import static de.otto.edison.hal.Embedded.Builder.copyOf; 14 | import static de.otto.edison.hal.Embedded.emptyEmbedded; 15 | import static de.otto.edison.hal.Links.copyOf; 16 | import static de.otto.edison.hal.Links.emptyLinks; 17 | import static java.util.Collections.reverse; 18 | 19 | /** 20 | * Representation used to parse and create HAL+JSON documents from Java classes. 21 | * 22 | * @see hal_specification.html 23 | * @see draft-kelly-json-hal-08 24 | * 25 | * @since 0.1.0 26 | */ 27 | @JsonIgnoreProperties(ignoreUnknown = true) 28 | @JsonPropertyOrder(alphabetic = false) 29 | public class HalRepresentation { 30 | 31 | @JsonProperty(value = "_links") 32 | @JsonInclude(NON_NULL) 33 | private volatile Links links; 34 | @JsonProperty(value = "_embedded") 35 | @JsonInclude(NON_NULL) 36 | private volatile Embedded embedded; 37 | @JsonAnySetter 38 | private Map attributes = new LinkedHashMap<>(); 39 | @JsonIgnore 40 | private volatile Curies curies; 41 | 42 | /** 43 | * 44 | * @since 0.1.0 45 | */ 46 | public HalRepresentation() { 47 | this(null, null, emptyCuries()); 48 | } 49 | 50 | /** 51 | * Creates a HalRepresentation having {@link Links} 52 | * 53 | * @param links the Links of the HalRepresentation 54 | * @since 0.1.0 55 | */ 56 | public HalRepresentation(final Links links) { 57 | this(links, null, emptyCuries()); 58 | } 59 | 60 | /** 61 | * Creates a HalRepresentation having {@link Links} and a Curies that can be used 62 | * to configure the link-relation types that should always be rendered as an array of links. 63 | * 64 | * @param links the Links of the HalRepresentation 65 | * @param curies the Curies used to resolve curies 66 | * @since 1.0.0 67 | * @deprecated This method will most likely not be required by any users of edison-hal. Please contact me, 68 | * if you need this, otherwise the constructor will be removed in 3.0.0 69 | */ 70 | @Deprecated 71 | public HalRepresentation(final Links links, final Curies curies) { 72 | this(links, null, curies); 73 | } 74 | 75 | /** 76 | *

77 | * Creates a HalRepresentation with {@link Links} and {@link Embedded} objects. 78 | *

79 | *

80 | * If the Links do contain CURIs, the link-relation types of the embedded objects are shortened. 81 | *

82 | * 83 | * @param links the Links of the HalRepresentation 84 | * @param embedded the Embedded items of the HalRepresentation 85 | * @since 0.1.0 86 | */ 87 | @JsonCreator 88 | public HalRepresentation(final @JsonProperty("_links") Links links, 89 | final @JsonProperty("_embedded") Embedded embedded) { 90 | this.curies = emptyCuries(); 91 | this.links = links == null || links.isEmpty() 92 | ? null 93 | : links.using(this.curies); 94 | this.embedded = embedded == null || embedded.isEmpty() 95 | ? null 96 | : embedded.using(curies); 97 | } 98 | 99 | /** 100 | *

101 | * Creates a HalRepresentation with {@link Links}, {@link Embedded} objects and a Curies used to 102 | * resolve curies from parent representations. 103 | *

104 | *

105 | * If the Links do contain CURIs, the matching link-relation types of links and embedded objects are shortened. 106 | *

107 | * 108 | * @param links the Links of the HalRepresentation 109 | * @param embedded the Embedded items of the HalRepresentation 110 | * @param curies the Curies used to resolve curies 111 | * @since 1.0.0 112 | * @deprecated This method will most likely not be required by any users of edison-hal. Please contact me, 113 | * if you need this, otherwise the constructor will be removed in 3.0.0 114 | */ 115 | @Deprecated 116 | public HalRepresentation(final Links links, 117 | final Embedded embedded, 118 | final Curies curies) { 119 | this.curies = curies; 120 | this.links = links == null || links.isEmpty() 121 | ? null 122 | : links.using(this.curies); 123 | this.embedded = embedded == null || embedded.isEmpty() 124 | ? null 125 | : embedded.using(this.curies); 126 | } 127 | 128 | /** 129 | * 130 | * @return the Curies used by this HalRepresentation. 131 | */ 132 | Curies getCuries() { 133 | return curies; 134 | } 135 | 136 | /** 137 | * Returns the Links of the HalRepresentation. 138 | * 139 | * @return Links 140 | * @since 0.1.0 141 | */ 142 | @JsonIgnore 143 | public Links getLinks() { 144 | return links != null ? links : emptyLinks(); 145 | } 146 | 147 | /** 148 | * Add links to the HalRepresentation. 149 | *

150 | * Links are only added if they are not {@link Link#isEquivalentTo(Link) equivalent} 151 | * to already existing links. 152 | *

153 | * @param links links that are added to this HalRepresentation 154 | * @return this 155 | */ 156 | protected HalRepresentation add(final Links links) { 157 | this.links = this.links != null 158 | ? copyOf(this.links).with(links).build() 159 | : links.using(this.curies); 160 | if (embedded != null) { 161 | embedded = embedded.using(this.curies); 162 | } 163 | return this; 164 | } 165 | 166 | /** 167 | * Returns the Embedded objects of the HalRepresentation. 168 | * 169 | * @return Embedded, possibly beeing {@link Embedded#isEmpty() empty} 170 | */ 171 | @JsonIgnore 172 | public Embedded getEmbedded() { 173 | return embedded != null ? embedded : emptyEmbedded(); 174 | } 175 | 176 | /** 177 | * Returns extra attributes that were not mapped to properties of the HalRepresentation. 178 | * 179 | * @return map containing unmapped attributes 180 | */ 181 | @JsonAnyGetter 182 | public Map getAttributes() { 183 | if (!attributes.isEmpty()) { 184 | // For some reason, Jackson is reversing the order of extra attributes. 185 | // In order to avoid confusion, the attribute ordering is reversed here: 186 | Map orderedMap = new LinkedHashMap<>(); 187 | List keys = new ArrayList<>(attributes.keySet()); 188 | reverse(keys); 189 | keys.forEach(k -> orderedMap.put(k, attributes.get(k))); 190 | return orderedMap; 191 | } else { 192 | return attributes; 193 | } 194 | } 195 | 196 | /** 197 | * Returns the value of an extra attribute as a JsonNode, or null if no such attribute is present. 198 | * 199 | * @param name the name of the attribute 200 | * @return JsonNode or null 201 | */ 202 | @JsonIgnore 203 | public JsonNode getAttribute(final String name) { 204 | return attributes.get(name); 205 | } 206 | 207 | /** 208 | * Adds embedded items for a link-relation type to the HalRepresentation. 209 | *

210 | * If {@code rel} is already present, it is replaced by the new embedded items. 211 | *

212 | * 213 | * @param rel the link-relation type of the embedded items that are added or replaced 214 | * @param embeddedItems the new values for the specified link-relation type 215 | * @return this 216 | * @since 0.5.0 217 | */ 218 | protected HalRepresentation withEmbedded(final String rel, final List embeddedItems) { 219 | embedded = copyOf(embedded).with(rel, embeddedItems).using(curies).build(); 220 | return this; 221 | } 222 | 223 | /** 224 | * Adds an embedded item for a link-relation type to the HalRepresentation. 225 | *

226 | * The embedded item will be rendered as a single resource object. 227 | *

228 | *

229 | * If {@code rel} is already present, it is replaced by the new embedded items. 230 | *

231 | * 232 | * @param rel the link-relation type of the embedded item that is added or replaced 233 | * @param embeddedItem the new value for the specified link-relation type 234 | * @return this 235 | * 236 | * @since 2.0.0 237 | */ 238 | protected HalRepresentation withEmbedded(final String rel, final HalRepresentation embeddedItem) { 239 | embedded = copyOf(embedded).with(rel, embeddedItem).using(curies).build(); 240 | return this; 241 | } 242 | 243 | /** 244 | * Merges the Curies of an embedded resource with the Curies of this resource and updates 245 | * link-relation types in _links and _embedded items. 246 | * 247 | * @param curies the Curies of the embedding resource 248 | * @return this 249 | */ 250 | HalRepresentation mergeWithEmbedding(final Curies curies) { 251 | this.curies = this.curies.mergeWith(curies); 252 | if (this.links != null) { 253 | 254 | removeDuplicateCuriesFromEmbedding(curies); 255 | 256 | this.links = this.links.using(this.curies); 257 | if (embedded != null) { 258 | embedded = embedded.using(this.curies); 259 | } 260 | } else { 261 | if (embedded != null) { 262 | embedded = embedded.using(curies); 263 | } 264 | } 265 | return this; 266 | } 267 | 268 | private void removeDuplicateCuriesFromEmbedding(final Curies curies) { 269 | if (this.links.hasLink("curies")) { 270 | final List curiLinks = new ArrayList<>(this.links.getLinksBy("curies")); 271 | curies.getCuries().forEach(curi -> { 272 | curiLinks.removeIf((link -> link.isEquivalentTo(curi))); 273 | }); 274 | this.links = copyOf(this.links).replace("curies", curiLinks).build(); 275 | } 276 | } 277 | 278 | /** 279 | * {@inheritDoc} 280 | * 281 | * @since 0.1.0 282 | */ 283 | @Override 284 | public boolean equals(Object o) { 285 | if (this == o) return true; 286 | if (o == null || getClass() != o.getClass()) return false; 287 | 288 | HalRepresentation that = (HalRepresentation) o; 289 | 290 | if (links != null ? !links.equals(that.links) : that.links != null) return false; 291 | return embedded != null ? embedded.equals(that.embedded) : that.embedded == null; 292 | 293 | } 294 | 295 | /** 296 | * {@inheritDoc} 297 | * 298 | * @since 0.1.0 299 | */ 300 | @Override 301 | public int hashCode() { 302 | int result = links != null ? links.hashCode() : 0; 303 | result = 31 * result + (embedded != null ? embedded.hashCode() : 0); 304 | return result; 305 | } 306 | 307 | /** 308 | * {@inheritDoc} 309 | * 310 | * @since 0.1.0 311 | */ 312 | @Override 313 | public String toString() { 314 | return "HalRepresentation{" + 315 | "links=" + links + 316 | ", embedded=" + embedded + 317 | '}'; 318 | } 319 | } 320 | --------------------------------------------------------------------------------