├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── CODE_OF_CONDUCT.adoc ├── Jenkinsfile ├── LICENSE ├── README.adoc ├── affordances ├── README.adoc ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── springframework │ │ └── hateoas │ │ └── examples │ │ ├── DatabaseLoader.java │ │ ├── Employee.java │ │ ├── EmployeeController.java │ │ ├── EmployeeRepository.java │ │ ├── HypermediaConfiguration.java │ │ └── SpringHateoasAffordancesApplication.java │ └── test │ └── java │ └── org │ └── springframework │ └── hateoas │ └── examples │ └── EmployeeControllerTests.java ├── api-evolution ├── README.adoc ├── new-client │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── org │ │ │ └── springframework │ │ │ └── hateoas │ │ │ └── examples │ │ │ ├── ClientConfig.java │ │ │ ├── Employee.java │ │ │ ├── HomeController.java │ │ │ └── NewClientApplication.java │ │ └── resources │ │ └── templates │ │ └── index.html ├── new-server │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── org │ │ │ └── springframework │ │ │ └── hateoas │ │ │ └── examples │ │ │ ├── Employee.java │ │ │ ├── EmployeeController.java │ │ │ ├── EmployeeRepository.java │ │ │ ├── EmployeeResourceAssembler.java │ │ │ ├── InitDatabase.java │ │ │ └── NewServerApplication.java │ │ └── resources │ │ └── application.yml ├── original-client │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── org │ │ │ └── springframework │ │ │ └── hateoas │ │ │ └── examples │ │ │ ├── ClientConfig.java │ │ │ ├── Employee.java │ │ │ ├── HomeController.java │ │ │ └── OriginalClientApplication.java │ │ └── resources │ │ └── templates │ │ └── index.html ├── original-server │ ├── pom.xml │ └── src │ │ └── main │ │ ├── java │ │ └── org │ │ │ └── springframework │ │ │ └── hateoas │ │ │ └── examples │ │ │ ├── Employee.java │ │ │ ├── EmployeeController.java │ │ │ ├── EmployeeRepository.java │ │ │ ├── EmployeeRepresentationModelAssembler.java │ │ │ ├── InitDatabase.java │ │ │ └── OriginalServerApplication.java │ │ └── resources │ │ └── application.yml └── pom.xml ├── basics ├── README.adoc ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── springframework │ │ └── hateoas │ │ └── examples │ │ ├── DatabaseLoader.java │ │ ├── Employee.java │ │ ├── EmployeeController.java │ │ ├── EmployeeRepository.java │ │ ├── EmployeeRepresentationModelAssembler.java │ │ └── SpringHateoasBasicsApplication.java │ └── test │ └── java │ └── org │ └── springframework │ └── hateoas │ └── examples │ └── EmployeeControllerTests.java ├── ci └── test.sh ├── commons ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── springframework │ └── hateoas │ └── SimpleIdentifiableRepresentationModelAssembler.java ├── hypermedia ├── README.adoc ├── pom.xml └── src │ └── main │ └── java │ └── org │ └── springframework │ └── hateoas │ └── examples │ ├── DatabaseLoader.java │ ├── Employee.java │ ├── EmployeeController.java │ ├── EmployeeRepository.java │ ├── EmployeeRepresentationModelAssembler.java │ ├── EmployeeWithManager.java │ ├── EmployeeWithManagerResourceAssembler.java │ ├── Manager.java │ ├── ManagerController.java │ ├── ManagerRepository.java │ ├── ManagerRepresentationModelAssembler.java │ ├── RootController.java │ ├── SpringHateoasHypermediaApplication.java │ ├── Supervisor.java │ └── SupervisorController.java ├── mvnw ├── mvnw.cmd ├── pom.xml ├── security └── pom.xml ├── simplified ├── README.adoc ├── pom.xml └── src │ ├── main │ └── java │ │ └── org │ │ └── springframework │ │ └── hateoas │ │ └── examples │ │ ├── DatabaseLoader.java │ │ ├── Employee.java │ │ ├── EmployeeController.java │ │ ├── EmployeeRepository.java │ │ └── SpringHateoasSimplifiedApplication.java │ └── test │ └── java │ └── org │ └── springframework │ └── hateoas │ └── examples │ └── EmployeeControllerTests.java └── spring-hateoas-and-spring-data-rest ├── README.adoc ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── springframework │ │ └── hateoas │ │ └── examples │ │ ├── CustomOrderController.java │ │ ├── DatabaseLoader.java │ │ ├── Order.java │ │ ├── OrderNotFoundException.java │ │ ├── OrderProcessor.java │ │ ├── OrderRepository.java │ │ ├── OrderStatus.java │ │ └── SpringHateoasSpringDataRestApplication.java └── resources │ └── application.yml └── test └── java └── org └── springframework └── hateoas └── examples └── OrderIntegrationTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/maven,java,intellij+iml,eclipse 3 | 4 | ### Eclipse ### 5 | 6 | .metadata 7 | bin/ 8 | tmp/ 9 | *.tmp 10 | *.bak 11 | *.swp 12 | *~.nib 13 | local.properties 14 | .settings/ 15 | .loadpath 16 | .recommenders 17 | 18 | # External tool builders 19 | .externalToolBuilders/ 20 | 21 | # Locally stored "Eclipse launch configurations" 22 | *.launch 23 | 24 | # PyDev specific (Python IDE for Eclipse) 25 | *.pydevproject 26 | 27 | # CDT-specific (C/C++ Development Tooling) 28 | .cproject 29 | 30 | # Java annotation processor (APT) 31 | .factorypath 32 | 33 | # PDT-specific (PHP Development Tools) 34 | .buildpath 35 | 36 | # sbteclipse plugin 37 | .target 38 | 39 | # Tern plugin 40 | .tern-project 41 | 42 | # TeXlipse plugin 43 | .texlipse 44 | 45 | # STS (Spring Tool Suite) 46 | .springBeans 47 | 48 | # Code Recommenders 49 | .recommenders/ 50 | 51 | # Scala IDE specific (Scala & Java development for Eclipse) 52 | .cache-main 53 | .scala_dependencies 54 | .worksheet 55 | 56 | ### Eclipse Patch ### 57 | # Eclipse Core 58 | .project 59 | 60 | # JDT-specific (Eclipse Java Development Tools) 61 | .classpath 62 | 63 | ### Intellij+iml ### 64 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 65 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 66 | 67 | # User-specific stuff: 68 | .idea 69 | 70 | # CMake 71 | cmake-build-debug/ 72 | 73 | # Mongo Explorer plugin: 74 | .idea/**/mongoSettings.xml 75 | 76 | ## File-based project format: 77 | *.iws 78 | 79 | ## Plugin-specific files: 80 | 81 | # IntelliJ 82 | /out/ 83 | 84 | # mpeltonen/sbt-idea plugin 85 | .idea_modules/ 86 | 87 | # JIRA plugin 88 | atlassian-ide-plugin.xml 89 | 90 | # Cursive Clojure plugin 91 | .idea/replstate.xml 92 | 93 | # Crashlytics plugin (for Android Studio and IntelliJ) 94 | com_crashlytics_export_strings.xml 95 | crashlytics.properties 96 | crashlytics-build.properties 97 | fabric.properties 98 | 99 | ### Intellij+iml Patch ### 100 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 101 | 102 | *.iml 103 | modules.xml 104 | .idea/misc.xml 105 | *.ipr 106 | 107 | ### Java ### 108 | # Compiled class file 109 | *.class 110 | 111 | # Log file 112 | *.log 113 | 114 | # BlueJ files 115 | *.ctxt 116 | 117 | # Mobile Tools for Java (J2ME) 118 | .mtj.tmp/ 119 | 120 | # Package Files # 121 | *.jar 122 | *.war 123 | *.ear 124 | *.zip 125 | *.tar.gz 126 | *.rar 127 | 128 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 129 | hs_err_pid* 130 | 131 | ### Maven ### 132 | target/ 133 | pom.xml.tag 134 | pom.xml.releaseBackup 135 | pom.xml.versionsBackup 136 | pom.xml.next 137 | release.properties 138 | dependency-reduced-pom.xml 139 | buildNumber.properties 140 | .mvn/timing.properties 141 | 142 | # Avoid ignoring Maven wrapper jar file (.jar files are usually ignored) 143 | !/.mvn/wrapper/maven-wrapper.jar 144 | 145 | # End of https://www.gitignore.io/api/maven,java,intellij+iml,eclipse 146 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spring-projects/spring-hateoas-examples/c100c56982473c60e8042f14e6052333a3ae1b55/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.0/apache-maven-3.5.0-bin.zip 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.adoc: -------------------------------------------------------------------------------- 1 | = Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | 9 | * The use of sexualized language or imagery 10 | * Personal attacks 11 | * Trolling or insulting/derogatory comments 12 | * Public or private harassment 13 | * Publishing other's private information, such as physical or electronic addresses, 14 | without explicit permission 15 | * Other unethical or unprofessional conduct 16 | 17 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 18 | 19 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 20 | 21 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 22 | 23 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting a project maintainer at spring-code-of-conduct@pivotal.io. 24 | All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. 25 | Maintainers are obligated to maintain confidentiality with regard to the reporter of an incident. 26 | 27 | This Code of Conduct is adapted from the http://contributor-covenant.org[Contributor Covenant], version 1.3.0, available at http://contributor-covenant.org/version/1/3/0/[contributor-covenant.org/version/1/3/0/]. -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | agent none 3 | 4 | triggers { 5 | pollSCM 'H/10 * * * *' 6 | } 7 | 8 | options { 9 | disableConcurrentBuilds() 10 | buildDiscarder(logRotator(numToKeepStr: '14')) 11 | } 12 | 13 | stages { 14 | stage("test: baseline (jdk8)") { 15 | agent { 16 | docker { 17 | image 'adoptopenjdk/openjdk8:latest' 18 | args '-v $HOME/.m2:/root/.m2' 19 | } 20 | } 21 | steps { 22 | sh "PROFILE=none ci/test.sh" 23 | } 24 | } 25 | 26 | stage("Test other configurations") { 27 | parallel { 28 | stage("test: baseline (jdk11)") { 29 | agent { 30 | docker { 31 | image 'adoptopenjdk/openjdk11:latest' 32 | args '-v $HOME/.m2:/root/.m2' 33 | } 34 | } 35 | steps { 36 | sh "PROFILE=none ci/test.sh" 37 | } 38 | } 39 | stage("test: baseline (jdk13)") { 40 | agent { 41 | docker { 42 | image 'adoptopenjdk/openjdk13:latest' 43 | args '-v $HOME/.m2:/root/.m2' 44 | } 45 | } 46 | steps { 47 | sh "PROFILE=none ci/test.sh" 48 | } 49 | } 50 | } 51 | } 52 | } 53 | 54 | post { 55 | changed { 56 | script { 57 | slackSend( 58 | color: (currentBuild.currentResult == 'SUCCESS') ? 'good' : 'danger', 59 | channel: '#spring-hateoas', 60 | message: "${currentBuild.fullDisplayName} - `${currentBuild.currentResult}`\n${env.BUILD_URL}") 61 | emailext( 62 | subject: "[${currentBuild.fullDisplayName}] ${currentBuild.currentResult}", 63 | mimeType: 'text/html', 64 | recipientProviders: [[$class: 'CulpritsRecipientProvider'], [$class: 'RequesterRecipientProvider']], 65 | body: "${currentBuild.fullDisplayName} is reported as ${currentBuild.currentResult}") 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | = Spring HATEOAS Examples 2 | 3 | image:https://jenkins.spring.io/buildStatus/icon?job=spring-hateoas-examples%2Fmain&subject=main[link=https://jenkins.spring.io/view/SpringHATEOAS/job/spring-hateoas-examples/] 4 | image:https://jenkins.spring.io/buildStatus/icon?job=spring-hateoas-examples%2F0.x&subject=0.x[link=https://jenkins.spring.io/view/SpringHATEOAS/job/spring-hateoas-examples/] 5 | 6 | This repository contains example projects to interact with Spring HATEOAS. 7 | 8 | * Learn how to interact with a Spring HATEOAS-powered app, from inside as well as the command line. 9 | * See how to upgrade a REST resource without having to create new media types, version URIs, etc. 10 | 11 | We have separate folders for each of these: 12 | 13 | == Spring HATEOAS Modules 14 | 15 | * link:basics[Basics] - Poke and prod at a hypermedia-powered service from inside the code as well as externally using standard tools 16 | * link:simplified[Simplified] - Use Spring HATEOAS in the simplest way possible. 17 | * link:api-evolution[API Evolution] - Upgrade an existing REST resource 18 | * link:hypermedia[Hypermedia] - Create hypermedia-driven REST resources, linking them together, and supporting older links. 19 | * link:affordances[Affordances] - Create richer hypermedia controls using more complex hypermedia formats 20 | * link:spring-hateoas-and-spring-data-rest[Spring HATEOAS + Spring Data REST] - How to stir in custom links and logic with a Spring Data REST-based app 21 | 22 | NOTE: The main branch tracks Spring HATEOAS 1.0, based upon Spring Boot 2 + Spring Framework 5. 23 | To see examples depicted against the 0.x branch (Spring 4.x) visit the https://github.com/spring-projects/spring-hateoas-examples/tree/0.x[0.x branch]. 24 | 25 | == Community 26 | 27 | The Spring HATEOAS community has its own contributions when it comes to examples of building hypermedia. 28 | 29 | * https://github.com/ingogriebsch/spring-hateoas-siren-samples[Siren HATEOAS Examples] - The maintainer of the Siren extension of Spring HATEOAS has an extra set of examples. 30 | 31 | -------------------------------------------------------------------------------- /affordances/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-affordances 8 | Spring HATEOAS - Examples - Affordances 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-maven-plugin 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /affordances/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.stereotype.Component; 21 | 22 | /** 23 | * Pre-load some data using a Spring Boot {@link CommandLineRunner}. 24 | * 25 | * @author Greg Turnquist 26 | */ 27 | @Component 28 | class DatabaseLoader { 29 | 30 | /** 31 | * Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run only after the app 32 | * is operational, the database will be up. 33 | * 34 | * @param repository 35 | */ 36 | @Bean 37 | CommandLineRunner init(EmployeeRepository repository) { 38 | 39 | return args -> { 40 | repository.save(new Employee("Frodo", "Baggins", "ring bearer")); 41 | repository.save(new Employee("Bilbo", "Baggins", "burglar")); 42 | }; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /affordances/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.AccessLevel; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Data; 21 | import lombok.NoArgsConstructor; 22 | 23 | import javax.persistence.Entity; 24 | import javax.persistence.GeneratedValue; 25 | import javax.persistence.Id; 26 | 27 | /** 28 | * Domain object representing a company employee. Project Lombok keeps actual code at a minimum. {@code @Data} - 29 | * Generates getters, setters, toString, hash, and equals functions {@code @Entity} - JPA annotation to flag this class 30 | * for DB persistence {@code @NoArgsConstructor} - Create a constructor with no args to support JPA 31 | * {@code @AllArgsConstructor} - Create a constructor with all args to support testing 32 | * {@code @JsonIgnoreProperties(ignoreUnknow=true)} When converting JSON to Java, ignore any unrecognized attributes. 33 | * This is critical for REST because it encourages adding new fields in later versions that won't break. It also allows 34 | * things like _links to be ignore as well, meaning HAL documents can be fetched and later posted to the server without 35 | * adjustment. 36 | * 37 | * @author Greg Turnquist 38 | */ 39 | @Data 40 | @Entity 41 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 42 | @AllArgsConstructor 43 | class Employee { 44 | 45 | @Id @GeneratedValue private Long id; 46 | private String firstName; 47 | private String lastName; 48 | private String role; 49 | 50 | /** 51 | * Useful constructor when id is not yet known. 52 | */ 53 | Employee(String firstName, String lastName, String role) { 54 | 55 | this.firstName = firstName; 56 | this.lastName = lastName; 57 | this.role = role; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /affordances/src/main/java/org/springframework/hateoas/examples/EmployeeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import java.net.URI; 21 | import java.net.URISyntaxException; 22 | import java.util.List; 23 | import java.util.stream.Collectors; 24 | import java.util.stream.StreamSupport; 25 | 26 | import org.springframework.hateoas.CollectionModel; 27 | import org.springframework.hateoas.EntityModel; 28 | import org.springframework.hateoas.IanaLinkRelations; 29 | import org.springframework.hateoas.Link; 30 | import org.springframework.http.ResponseEntity; 31 | import org.springframework.web.bind.annotation.DeleteMapping; 32 | import org.springframework.web.bind.annotation.GetMapping; 33 | import org.springframework.web.bind.annotation.PathVariable; 34 | import org.springframework.web.bind.annotation.PostMapping; 35 | import org.springframework.web.bind.annotation.PutMapping; 36 | import org.springframework.web.bind.annotation.RequestBody; 37 | import org.springframework.web.bind.annotation.RestController; 38 | 39 | /** 40 | * @author Greg Turnquist 41 | */ 42 | @RestController 43 | class EmployeeController { 44 | 45 | private final EmployeeRepository repository; 46 | 47 | EmployeeController(EmployeeRepository repository) { 48 | this.repository = repository; 49 | } 50 | 51 | @GetMapping("/employees") 52 | ResponseEntity>> findAll() { 53 | 54 | List> employeeResources = StreamSupport.stream(repository.findAll().spliterator(), false) 55 | .map(employee -> EntityModel.of(employee, 56 | linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel() 57 | .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId()))) 58 | .andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))), 59 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) 60 | .collect(Collectors.toList()); 61 | 62 | return ResponseEntity.ok(CollectionModel.of( // 63 | employeeResources, // 64 | linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel() 65 | .andAffordance(afford(methodOn(EmployeeController.class).newEmployee(null))))); 66 | } 67 | 68 | @PostMapping("/employees") 69 | ResponseEntity newEmployee(@RequestBody Employee employee) { 70 | 71 | Employee savedEmployee = repository.save(employee); 72 | 73 | return EntityModel.of(savedEmployee, 74 | linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel() 75 | .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, savedEmployee.getId()))) 76 | .andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(savedEmployee.getId()))), 77 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")).getLink(IanaLinkRelations.SELF) 78 | .map(Link::getHref) // 79 | .map(href -> { 80 | try { 81 | return new URI(href); 82 | } catch (URISyntaxException e) { 83 | throw new RuntimeException(e); 84 | } 85 | }) // 86 | .map(uri -> ResponseEntity.noContent().location(uri).build()) 87 | .orElse(ResponseEntity.badRequest().body("Unable to create " + employee)); 88 | } 89 | 90 | @GetMapping("/employees/{id}") 91 | ResponseEntity> findOne(@PathVariable long id) { 92 | 93 | return repository.findById(id) 94 | .map(employee -> EntityModel.of(employee, 95 | linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel() 96 | .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, employee.getId()))) 97 | .andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(employee.getId()))), 98 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) 99 | .map(ResponseEntity::ok) // 100 | .orElse(ResponseEntity.notFound().build()); 101 | } 102 | 103 | @PutMapping("/employees/{id}") 104 | ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { 105 | 106 | Employee employeeToUpdate = employee; 107 | employeeToUpdate.setId(id); 108 | 109 | Employee updatedEmployee = repository.save(employeeToUpdate); 110 | 111 | return EntityModel.of(updatedEmployee, 112 | linkTo(methodOn(EmployeeController.class).findOne(updatedEmployee.getId())).withSelfRel() 113 | .andAffordance(afford(methodOn(EmployeeController.class).updateEmployee(null, updatedEmployee.getId()))) 114 | .andAffordance(afford(methodOn(EmployeeController.class).deleteEmployee(updatedEmployee.getId()))), 115 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")).getLink(IanaLinkRelations.SELF) 116 | .map(Link::getHref).map(href -> { 117 | try { 118 | return new URI(href); 119 | } catch (URISyntaxException e) { 120 | throw new RuntimeException(e); 121 | } 122 | }) // 123 | .map(uri -> ResponseEntity.noContent().location(uri).build()) // 124 | .orElse(ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate)); 125 | } 126 | 127 | @DeleteMapping("/employees/{id}") 128 | ResponseEntity deleteEmployee(@PathVariable long id) { 129 | 130 | repository.deleteById(id); 131 | 132 | return ResponseEntity.noContent().build(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /affordances/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.data.repository.CrudRepository; 19 | 20 | /** 21 | * @author Greg Turnquist 22 | */ 23 | interface EmployeeRepository extends CrudRepository { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /affordances/src/main/java/org/springframework/hateoas/examples/HypermediaConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.context.annotation.Configuration; 19 | import org.springframework.hateoas.config.EnableHypermediaSupport; 20 | import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; 21 | 22 | /** 23 | * @author Greg Turnquist 24 | */ 25 | @Configuration 26 | @EnableHypermediaSupport(type = HypermediaType.HAL_FORMS) 27 | class HypermediaConfiguration { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /affordances/src/main/java/org/springframework/hateoas/examples/SpringHateoasAffordancesApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @SpringBootApplication 25 | public class SpringHateoasAffordancesApplication { 26 | 27 | public static void main(String... args) { 28 | SpringApplication.run(SpringHateoasAffordancesApplication.class, args); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /affordances/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.hamcrest.CoreMatchers.*; 19 | import static org.mockito.ArgumentMatchers.any; 20 | import static org.mockito.BDDMockito.*; 21 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 24 | 25 | import java.util.Arrays; 26 | import java.util.Optional; 27 | 28 | import org.junit.Test; 29 | import org.junit.runner.RunWith; 30 | import org.springframework.beans.factory.annotation.Autowired; 31 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 32 | import org.springframework.boot.test.mock.mockito.MockBean; 33 | import org.springframework.context.annotation.Import; 34 | import org.springframework.hateoas.MediaTypes; 35 | import org.springframework.http.HttpHeaders; 36 | import org.springframework.test.context.junit4.SpringRunner; 37 | import org.springframework.test.web.servlet.MockMvc; 38 | 39 | /** 40 | * @author Greg Turnquist 41 | */ 42 | @RunWith(SpringRunner.class) 43 | @WebMvcTest(EmployeeController.class) 44 | @Import({ HypermediaConfiguration.class }) 45 | public class EmployeeControllerTests { 46 | 47 | @Autowired private MockMvc mvc; 48 | 49 | @MockBean private EmployeeRepository repository; 50 | 51 | @Test 52 | public void getAllShouldFetchAHalFormsEmbeddedDocument() throws Exception { 53 | 54 | given(repository.findAll()).willReturn(Arrays.asList( // 55 | new Employee(1L, "Frodo", "Baggins", "ring bearer"), // 56 | new Employee(2L, "Bilbo", "Baggins", "burglar"))); 57 | 58 | mvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON_VALUE)) // 59 | .andDo(print()) // 60 | .andExpect(status().isOk()) // 61 | .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_FORMS_JSON_VALUE)) 62 | 63 | .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) 64 | .andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo"))) 65 | .andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins"))) 66 | .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) 67 | .andExpect(jsonPath("$._embedded.employees[0]._templates.default.method", is("put"))) 68 | .andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[0].name", is("firstName"))) 69 | .andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[1].name", is("id"))) 70 | .andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[2].name", is("lastName"))) 71 | .andExpect(jsonPath("$._embedded.employees[0]._templates.default.properties[3].name", is("role"))) 72 | .andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1"))) 73 | .andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees"))) 74 | 75 | .andExpect(jsonPath("$._embedded.employees[1].id", is(2))) 76 | .andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo"))) 77 | .andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins"))) 78 | .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) 79 | .andExpect(jsonPath("$._embedded.employees[1]._templates.default.method", is("put"))) 80 | .andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[0].name", is("firstName"))) 81 | .andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[1].name", is("id"))) 82 | .andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[2].name", is("lastName"))) 83 | .andExpect(jsonPath("$._embedded.employees[1]._templates.default.properties[3].name", is("role"))) 84 | .andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2"))) 85 | .andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees"))) 86 | 87 | .andExpect(jsonPath("$._templates.default.method", is("post"))) 88 | .andExpect(jsonPath("$._templates.default.properties[0].name", is("firstName"))) 89 | .andExpect(jsonPath("$._templates.default.properties[1].name", is("id"))) 90 | .andExpect(jsonPath("$._templates.default.properties[2].name", is("lastName"))) 91 | .andExpect(jsonPath("$._templates.default.properties[3].name", is("role"))) 92 | 93 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/employees"))); 94 | } 95 | 96 | @Test 97 | public void getOneShouldFetchASingleHalFormsDocument() throws Exception { 98 | 99 | given(repository.findById(any())).willReturn(Optional.of(new Employee(1L, "Frodo", "Baggins", "ring bearer"))); 100 | 101 | mvc.perform(get("/employees/1").accept(MediaTypes.HAL_FORMS_JSON_VALUE)) // 102 | .andDo(print()) // 103 | .andExpect(status().isOk()) // 104 | .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_FORMS_JSON_VALUE)) 105 | 106 | .andExpect(jsonPath("$.id", is(1))) // 107 | .andExpect(jsonPath("$.firstName", is("Frodo"))) // 108 | .andExpect(jsonPath("$.lastName", is("Baggins"))) // 109 | .andExpect(jsonPath("$.role", is("ring bearer"))) 110 | 111 | .andExpect(jsonPath("$._templates.default.method", is("put"))) 112 | .andExpect(jsonPath("$._templates.default.properties[0].name", is("firstName"))) 113 | .andExpect(jsonPath("$._templates.default.properties[1].name", is("id"))) 114 | .andExpect(jsonPath("$._templates.default.properties[2].name", is("lastName"))) 115 | .andExpect(jsonPath("$._templates.default.properties[3].name", is("role"))) 116 | 117 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/employees/1"))) 118 | .andExpect(jsonPath("$._links.employees.href", is("http://localhost/employees"))); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /api-evolution/new-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-new-client 8 | Spring HATEOAS - Examples - API Evolution - New Client 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples-api-evolution 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | 30 | com.jayway.jsonpath 31 | json-path 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-maven-plugin 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/ClientConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.web.client.RestTemplateCustomizer; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.context.annotation.Configuration; 21 | import org.springframework.hateoas.config.HypermediaRestTemplateConfigurer; 22 | 23 | /** 24 | * @author Greg Turnquist 25 | */ 26 | @Configuration 27 | public class ClientConfig { 28 | 29 | @Bean 30 | RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { 31 | return restTemplate -> { 32 | configurer.registerHypermediaTypes(restTemplate); 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | 21 | /** 22 | * An updated domain object on the client side. It doesn't need all the backward compatible bits that the new server 23 | * needs (unless this becomes a service of its own). 24 | * 25 | * @author Greg Turnquist 26 | */ 27 | @Data 28 | @NoArgsConstructor 29 | class Employee { 30 | 31 | private Long id; 32 | private String firstName; 33 | private String lastName; 34 | private String role; 35 | } 36 | -------------------------------------------------------------------------------- /api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/HomeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | 21 | import org.springframework.boot.web.client.RestTemplateBuilder; 22 | import org.springframework.hateoas.CollectionModel; 23 | import org.springframework.hateoas.EntityModel; 24 | import org.springframework.hateoas.Link; 25 | import org.springframework.hateoas.MediaTypes; 26 | import org.springframework.hateoas.client.Traverson; 27 | import org.springframework.hateoas.server.core.TypeReferences.CollectionModelType; 28 | import org.springframework.stereotype.Controller; 29 | import org.springframework.ui.Model; 30 | import org.springframework.web.bind.annotation.GetMapping; 31 | import org.springframework.web.bind.annotation.ModelAttribute; 32 | import org.springframework.web.bind.annotation.PostMapping; 33 | import org.springframework.web.client.RestTemplate; 34 | 35 | /** 36 | * A web controller that serves up client data found on a remote REST service. 37 | * 38 | * @author Greg Turnquist 39 | */ 40 | @Controller 41 | public class HomeController { 42 | 43 | private static final String REMOTE_SERVICE_ROOT_URI = "http://localhost:9000"; 44 | 45 | private RestTemplate rest; 46 | 47 | public HomeController(RestTemplateBuilder restTemplateBuilder) { 48 | this.rest = restTemplateBuilder.build(); 49 | } 50 | 51 | /** 52 | * Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then "hopping" to the 53 | * {@literal employees} rel. NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry 54 | * with the Thymeleaf template. 55 | * 56 | * @param model 57 | * @return 58 | * @throws URISyntaxException 59 | */ 60 | @GetMapping 61 | public String index(Model model) throws URISyntaxException { 62 | 63 | Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON); 64 | 65 | CollectionModel> employees = client // 66 | .follow("employees") // 67 | .toObject(new CollectionModelType>() {}); 68 | 69 | model.addAttribute("employee", new Employee()); 70 | model.addAttribute("employees", employees); 71 | 72 | return "index"; 73 | } 74 | 75 | /** 76 | * Instead of putting the creation link from the remote service in the template (a security concern), have a local 77 | * route for {@literal POST} requests. Gather up the information, and form a remote call, using {@link Traverson} to 78 | * fetch the {@literal employees} {@link Link}. Once a new employee is created, redirect back to the root URL. 79 | * 80 | * @param employee 81 | * @return 82 | * @throws URISyntaxException 83 | */ 84 | @PostMapping("/employees") 85 | public String newEmployee(@ModelAttribute Employee employee) throws URISyntaxException { 86 | 87 | Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON); 88 | Link employeesLink = client.follow("employees").asLink(); 89 | 90 | this.rest.postForEntity(employeesLink.expand().getHref(), employee, Employee.class); 91 | 92 | return "redirect:/"; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /api-evolution/new-client/src/main/java/org/springframework/hateoas/examples/NewClientApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @SpringBootApplication 25 | public class NewClientApplication { 26 | 27 | public static void main(String... args) { 28 | SpringApplication.run(NewClientApplication.class, args); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /api-evolution/new-client/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring HATEOAS Examples - Original Client 6 | 17 | 18 | 19 |

Spring HATEOAS Examples - New Client

20 | 21 |

22 | This is the new client. It can only talk to the new server. If it were to become a REST service 23 | as well, we'd have to design a little extra in order to support that as well. 24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 44 | 45 | 46 |
First NameLast NameRoleLinks
35 | 36 | 37 | 38 | 43 |
47 | 48 |
49 | 50 | 51 | 52 | 53 |
54 | 55 | -------------------------------------------------------------------------------- /api-evolution/new-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-new-server 8 | Spring HATEOAS - Examples - API Evolution - New Server 9 | 1.0.0.BUILD-SNAPSHOT 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples-api-evolution 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-maven-plugin 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | 21 | import java.util.Arrays; 22 | import java.util.Optional; 23 | 24 | import javax.persistence.Entity; 25 | import javax.persistence.GeneratedValue; 26 | import javax.persistence.Id; 27 | 28 | import org.springframework.util.StringUtils; 29 | 30 | /** 31 | * An updated domain object where {@literal name} has been replaced by {@literal firstName} and {@literal} lastName. To 32 | * easy migration, we need to support the old {@literal name} field with a getter and a setter. 33 | * 34 | * @author Greg Turnquist 35 | */ 36 | @Data 37 | @NoArgsConstructor 38 | @Entity 39 | class Employee { 40 | 41 | @Id @GeneratedValue private Long id; 42 | private String firstName; 43 | private String lastName; 44 | private String role; 45 | 46 | Employee(String firstName, String lastName, String role) { 47 | 48 | this.firstName = firstName; 49 | this.lastName = lastName; 50 | this.role = role; 51 | } 52 | 53 | public Optional getId() { 54 | return Optional.ofNullable(this.id); 55 | } 56 | 57 | /** 58 | * Just merge {@literal firstName} and {@literal lastName} together. 59 | * 60 | * @return 61 | */ 62 | public String getName() { 63 | return this.firstName + " " + this.lastName; 64 | } 65 | 66 | /** 67 | * Split things up, and assign the first token to {@literal firstName} with everything else to {@literal lastName}. 68 | * 69 | * @param wholeName 70 | */ 71 | public void setName(String wholeName) { 72 | 73 | String[] parts = wholeName.split(" "); 74 | this.firstName = parts[0]; 75 | if (parts.length > 1) { 76 | this.lastName = StringUtils.arrayToDelimitedString(Arrays.copyOfRange(parts, 1, parts.length), " "); 77 | } else { 78 | this.lastName = ""; 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/EmployeeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import org.springframework.hateoas.CollectionModel; 21 | import org.springframework.hateoas.EntityModel; 22 | import org.springframework.hateoas.RepresentationModel; 23 | import org.springframework.http.ResponseEntity; 24 | import org.springframework.web.bind.annotation.GetMapping; 25 | import org.springframework.web.bind.annotation.PathVariable; 26 | import org.springframework.web.bind.annotation.PostMapping; 27 | import org.springframework.web.bind.annotation.RequestBody; 28 | import org.springframework.web.bind.annotation.RestController; 29 | 30 | /** 31 | * @author Greg Turnquist 32 | */ 33 | @RestController("/api") 34 | class EmployeeController { 35 | 36 | private final EmployeeRepository repository; 37 | private final EmployeeResourceAssembler assembler; 38 | 39 | EmployeeController(EmployeeRepository repository, EmployeeResourceAssembler assembler) { 40 | 41 | this.repository = repository; 42 | this.assembler = assembler; 43 | } 44 | 45 | @GetMapping("/") 46 | public RepresentationModel root() { 47 | 48 | RepresentationModel rootResource = new RepresentationModel(); 49 | 50 | rootResource.add( // 51 | linkTo(methodOn(EmployeeController.class).root()).withSelfRel(), // 52 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); 53 | 54 | return rootResource; 55 | } 56 | 57 | @GetMapping("/employees") 58 | public CollectionModel> findAll() { 59 | return assembler.toCollectionModel(repository.findAll()); 60 | } 61 | 62 | @PostMapping("/employees") 63 | public ResponseEntity> newEmployee(@RequestBody Employee employee) { 64 | 65 | Employee savedEmployee = repository.save(employee); 66 | 67 | return savedEmployee.getId() // 68 | .map(id -> ResponseEntity.created( // 69 | linkTo(methodOn(EmployeeController.class).findOne(id)).toUri()).body(assembler.toModel(savedEmployee))) 70 | .orElse(ResponseEntity.notFound().build()); 71 | } 72 | 73 | @GetMapping("/employees/{id}") 74 | public ResponseEntity> findOne(@PathVariable Long id) { 75 | 76 | return repository.findById(id) // 77 | .map(assembler::toModel) // 78 | .map(ResponseEntity::ok) // 79 | .orElse(ResponseEntity.notFound().build()); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.data.repository.CrudRepository; 19 | 20 | /** 21 | * @author Greg Turnquist 22 | */ 23 | interface EmployeeRepository extends CrudRepository { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/EmployeeResourceAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler; 19 | import org.springframework.stereotype.Component; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @Component 25 | class EmployeeResourceAssembler extends SimpleIdentifiableRepresentationModelAssembler { 26 | 27 | EmployeeResourceAssembler() { 28 | super(EmployeeController.class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/InitDatabase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.stereotype.Component; 21 | 22 | /** 23 | * @author Greg Turnquist 24 | */ 25 | @Component 26 | class InitDatabase { 27 | 28 | private final EmployeeRepository repository; 29 | 30 | InitDatabase(EmployeeRepository repository) { 31 | this.repository = repository; 32 | } 33 | 34 | @Bean 35 | CommandLineRunner loadEmployees() { 36 | 37 | return args -> { 38 | repository.save(new Employee("Frodo", "Baggins", "ring bearer")); 39 | repository.save(new Employee("Bilbo", "Baggins", "burglar")); 40 | }; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/java/org/springframework/hateoas/examples/NewServerApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @SpringBootApplication 25 | public class NewServerApplication { 26 | 27 | public static void main(String... args) { 28 | SpringApplication.run(NewServerApplication.class, args); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /api-evolution/new-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9000 -------------------------------------------------------------------------------- /api-evolution/original-client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-original-client 8 | Spring HATEOAS - Examples - API Evolution - Original Client 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples-api-evolution 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-thymeleaf 27 | 28 | 29 | 30 | com.jayway.jsonpath 31 | json-path 32 | 33 | 34 | 35 | 36 | 37 | 38 | org.springframework.boot 39 | spring-boot-maven-plugin 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/ClientConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.web.client.RestTemplateCustomizer; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.context.annotation.Configuration; 21 | import org.springframework.hateoas.config.HypermediaRestTemplateConfigurer; 22 | 23 | /** 24 | * @author Greg Turnquist 25 | */ 26 | @Configuration 27 | public class ClientConfig { 28 | 29 | @Bean 30 | RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer configurer) { 31 | return restTemplate -> { 32 | configurer.registerHypermediaTypes(restTemplate); 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @Data 25 | @NoArgsConstructor 26 | class Employee { 27 | 28 | private Long id; 29 | private String name; 30 | private String role; 31 | } 32 | -------------------------------------------------------------------------------- /api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/HomeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import java.net.URI; 19 | import java.net.URISyntaxException; 20 | 21 | import org.springframework.boot.web.client.RestTemplateBuilder; 22 | import org.springframework.hateoas.CollectionModel; 23 | import org.springframework.hateoas.EntityModel; 24 | import org.springframework.hateoas.Link; 25 | import org.springframework.hateoas.MediaTypes; 26 | import org.springframework.hateoas.client.Traverson; 27 | import org.springframework.hateoas.server.core.TypeReferences.CollectionModelType; 28 | import org.springframework.stereotype.Controller; 29 | import org.springframework.ui.Model; 30 | import org.springframework.web.bind.annotation.GetMapping; 31 | import org.springframework.web.bind.annotation.ModelAttribute; 32 | import org.springframework.web.bind.annotation.PostMapping; 33 | import org.springframework.web.client.RestTemplate; 34 | 35 | /** 36 | * A web controller that serves up client data found on a remote REST service. 37 | * 38 | * @author Greg Turnquist 39 | */ 40 | @Controller 41 | public class HomeController { 42 | 43 | private static final String REMOTE_SERVICE_ROOT_URI = "http://localhost:9000"; 44 | 45 | private final RestTemplate rest; 46 | 47 | public HomeController(RestTemplateBuilder restTemplateBuilder) { 48 | this.rest = restTemplateBuilder.build(); 49 | } 50 | 51 | /** 52 | * Get a listing of ALL {@link Employee}s by querying the remote services' root URI, and then "hopping" to the 53 | * {@literal employees} rel. NOTE: Also create a form-backed {@link Employee} object to allow creating a new entry 54 | * with the Thymeleaf template. 55 | * 56 | * @param model 57 | * @return 58 | * @throws URISyntaxException 59 | */ 60 | @GetMapping 61 | public String index(Model model) throws URISyntaxException { 62 | 63 | Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON); 64 | CollectionModel> employees = client // 65 | .follow("employees") // 66 | .toObject(new CollectionModelType>() {}); 67 | 68 | model.addAttribute("employee", new Employee()); 69 | model.addAttribute("employees", employees); 70 | 71 | return "index"; 72 | } 73 | 74 | /** 75 | * Instead of putting the creation link from the remote service in the template (a security concern), have a local 76 | * route for {@literal POST} requests. Gather up the information, and form a remote call, using {@link Traverson} to 77 | * fetch the {@literal employees} {@link Link}. Once a new employee is created, redirect back to the root URL. 78 | * 79 | * @param employee 80 | * @return 81 | * @throws URISyntaxException 82 | */ 83 | @PostMapping("/employees") 84 | public String newEmployee(@ModelAttribute Employee employee) throws URISyntaxException { 85 | 86 | Traverson client = new Traverson(new URI(REMOTE_SERVICE_ROOT_URI), MediaTypes.HAL_JSON); 87 | Link employeesLink = client // 88 | .follow("employees") // 89 | .asLink(); 90 | 91 | this.rest.postForEntity(employeesLink.expand().getHref(), employee, Employee.class); 92 | 93 | return "redirect:/"; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /api-evolution/original-client/src/main/java/org/springframework/hateoas/examples/OriginalClientApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @SpringBootApplication 25 | public class OriginalClientApplication { 26 | 27 | public static void main(String... args) { 28 | SpringApplication.run(OriginalClientApplication.class, args); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /api-evolution/original-client/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Spring HATEOAS Examples - Original Client 6 | 17 | 18 | 19 |

Spring HATEOAS Examples - Original Client

20 | 21 |

22 | This is the original client, and it was coded to talk to the original server, 23 | but with a little design and thought, we can have it to talk to the new server as well! 24 |

25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 43 | 44 | 45 |
NameRoleLinks
35 | 36 | 37 | 42 |
46 | 47 |
48 | 49 | 50 | 51 |
52 | 53 | -------------------------------------------------------------------------------- /api-evolution/original-server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-original-server 8 | Spring HATEOAS - Examples - API Evolution - Original Server 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples-api-evolution 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | 20 | org.springframework.boot 21 | spring-boot-maven-plugin 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.AccessLevel; 19 | import lombok.Data; 20 | import lombok.NoArgsConstructor; 21 | 22 | import java.util.Optional; 23 | 24 | import javax.persistence.Entity; 25 | import javax.persistence.GeneratedValue; 26 | import javax.persistence.Id; 27 | 28 | /** 29 | * @author Greg Turnquist 30 | */ 31 | @Data 32 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 33 | @Entity 34 | class Employee { 35 | 36 | @Id @GeneratedValue private Long id; 37 | private String name; 38 | private String role; 39 | 40 | Employee(String name, String role) { 41 | 42 | this.name = name; 43 | this.role = role; 44 | } 45 | 46 | public Optional getId() { 47 | return Optional.ofNullable(this.id); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import org.springframework.hateoas.CollectionModel; 21 | import org.springframework.hateoas.EntityModel; 22 | import org.springframework.hateoas.RepresentationModel; 23 | import org.springframework.http.ResponseEntity; 24 | import org.springframework.web.bind.annotation.GetMapping; 25 | import org.springframework.web.bind.annotation.PathVariable; 26 | import org.springframework.web.bind.annotation.PostMapping; 27 | import org.springframework.web.bind.annotation.RequestBody; 28 | import org.springframework.web.bind.annotation.RestController; 29 | 30 | /** 31 | * @author Greg Turnquist 32 | */ 33 | @RestController 34 | class EmployeeController { 35 | 36 | private final EmployeeRepository repository; 37 | private final EmployeeRepresentationModelAssembler assembler; 38 | 39 | EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler) { 40 | 41 | this.repository = repository; 42 | this.assembler = assembler; 43 | } 44 | 45 | @GetMapping("/") 46 | public RepresentationModel root() { 47 | 48 | RepresentationModel rootResource = new RepresentationModel(); 49 | 50 | rootResource.add( // 51 | linkTo(methodOn(EmployeeController.class).root()).withSelfRel(), // 52 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); 53 | 54 | return rootResource; 55 | } 56 | 57 | @GetMapping("/employees") 58 | public CollectionModel> findAll() { 59 | return assembler.toCollectionModel(repository.findAll()); 60 | } 61 | 62 | @PostMapping("/employees") 63 | public ResponseEntity> newEmployee(@RequestBody Employee employee) { 64 | 65 | Employee savedEmployee = repository.save(employee); 66 | 67 | return ResponseEntity // 68 | .created(savedEmployee.getId() // 69 | .map(id -> linkTo(methodOn(EmployeeController.class).findOne(id)).toUri()) // 70 | .orElseThrow(() -> new RuntimeException("Failed to create for some reason"))) // 71 | .body(assembler.toModel(savedEmployee)); 72 | } 73 | 74 | @GetMapping("/employees/{id}") 75 | public EntityModel findOne(@PathVariable Long id) { 76 | return repository.findById(id) // 77 | .map(assembler::toModel) // 78 | .orElseThrow(() -> new RuntimeException("No employee '" + id + "' found")); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.data.repository.CrudRepository; 19 | 20 | /** 21 | * @author Greg Turnquist 22 | */ 23 | interface EmployeeRepository extends CrudRepository { 24 | 25 | } 26 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/EmployeeRepresentationModelAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler; 19 | import org.springframework.stereotype.Component; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @Component 25 | class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentationModelAssembler { 26 | 27 | EmployeeRepresentationModelAssembler() { 28 | super(EmployeeController.class); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/InitDatabase.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.stereotype.Component; 21 | 22 | /** 23 | * @author Greg Turnquist 24 | */ 25 | @Component 26 | class InitDatabase { 27 | 28 | private final EmployeeRepository repository; 29 | 30 | InitDatabase(EmployeeRepository repository) { 31 | this.repository = repository; 32 | } 33 | 34 | @Bean 35 | CommandLineRunner loadEmployees() { 36 | 37 | return args -> { 38 | repository.save(new Employee("Frodo", "ring bearer")); 39 | repository.save(new Employee("Bilbo", "burglar")); 40 | }; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/java/org/springframework/hateoas/examples/OriginalServerApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @SpringBootApplication 25 | public class OriginalServerApplication { 26 | 27 | public static void main(String... args) { 28 | SpringApplication.run(OriginalServerApplication.class, args); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /api-evolution/original-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9000 -------------------------------------------------------------------------------- /api-evolution/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-api-evolution 8 | Spring HATEOAS - Examples - API Evolution 9 | pom 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | original-server 19 | original-client 20 | new-server 21 | new-client 22 | 23 | 24 | 25 | 26 | org.springframework.hateoas.examples 27 | commons 28 | 1.0.0.BUILD-SNAPSHOT 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /basics/README.adoc: -------------------------------------------------------------------------------- 1 | = Spring HATEOAS - Basic Example 2 | 3 | This guides shows a core example of using Spring HATEOAS. It illustrates how to sew hypermedia into your Spring MVC application, including test. 4 | 5 | Start with a very simple example, a payroll system that tracks employees. 6 | 7 | NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. 8 | 9 | == Defining Your Domain 10 | 11 | The cornerstone of any example is the domain object: 12 | 13 | [source,java] 14 | ---- 15 | @Data 16 | @Entity 17 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 18 | @AllArgsConstructor 19 | @JsonIgnoreProperties(ignoreUnknown = true) 20 | class Employee { 21 | 22 | @Id @GeneratedValue 23 | private Long id; 24 | 25 | private String firstName; 26 | 27 | private String lastName; 28 | 29 | private String role; 30 | 31 | ... 32 | } 33 | ---- 34 | 35 | This domain object includes: 36 | 37 | * `@Data` - Lombok annotation to define a mutable value object 38 | * `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) 39 | * `@NoArgsConstructor(PROTECTED)` - Lombok annotation to create an empty constructor call to appease JPA, but which is protected and not usable to our app's code. 40 | * `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios 41 | * `@JsonIgnoreProperties(ignoreUnknown=true)` - Jackson annotation to ignore unknown attributes when deserializing JSON. 42 | 43 | NOTE: Make your REST resources robust by instructing Jackson to ignore unknown attributes. That way, additional elements, 44 | like hypermedia (HAL *_links*, HAL-Forms *_templates*, etc.) and newer attributes will be discarded. 45 | 46 | This means a HAL document like this: 47 | 48 | ---- 49 | { 50 | "firstName": "Frodo", 51 | "lastName": "Baggins", 52 | "role" : "ring bearer", 53 | "_links" : { 54 | "self" : { 55 | "href" : "/employees/1" 56 | } 57 | } 58 | } 59 | ---- 60 | 61 | ...which was served up as hypermedia, can be POST'd to create a new employee. The *_links* will simply be ignored. 62 | 63 | == Accessing Data 64 | 65 | To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource. 66 | And while it's not a requirement for Spring HATEOAS, this example uses Spring Data JPA. 67 | 68 | Create a repository like this: 69 | 70 | [source,java] 71 | ---- 72 | interface EmployeeRepository extends CrudRepository { 73 | } 74 | ---- 75 | 76 | This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD) 77 | operations. 78 | 79 | [[converting-entities-to-resources]] 80 | == Converting Entities to Resources 81 | 82 | In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as information on _how_ to 83 | retrieve and update that information. 84 | 85 | Spring HATEOAS defines a generic `EntityModel` container used to model resources that lets store any domain object (`Employee` in this example), and 86 | add additional links. 87 | 88 | IMPORTANT: Spring HATEOAS's `EntityModel` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the 89 | default media type, but these classes can be used to render any media type. 90 | 91 | A common convention is to create a factory used to convert `Employee` objects into `EntityModel` objects. Spring 92 | HATEOAS provides `SimpleIdentifiableRepresentationModelAssembler` as the simplest mechanism to perform these conversions. 93 | 94 | [source,java] 95 | ---- 96 | @Component 97 | class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentationModelAssembler { 98 | 99 | /** 100 | * Link the {@link Employee} domain type to the {@link EmployeeController} using this 101 | * {@link SimpleIdentifiableRepresentationModelAssembler} in order to generate both {@link org.springframework.hateoas.EntityModel} 102 | * and {@link org.springframework.hateoas.CollectionModel}. 103 | */ 104 | EmployeeRepresentationModelAssembler() { 105 | super(EmployeeController.class); 106 | } 107 | } 108 | ---- 109 | 110 | This class has two key properties: 111 | 112 | * The generic type (`Employee`) declares the entity type. 113 | * The constructor defines the Spring MVC controller (`EmployeeController`) where links are found to turn the entity into a REST resource. 114 | 115 | The class is flagged with Spring's `@Component` annotation so that it will be automatically hooked into the 116 | application context. 117 | 118 | This is half the battle, already solved. This resource assembler is used _in the controller_ to assemble REST resources, as shown in the next section. 119 | 120 | == Creating Links 121 | 122 | The following Spring MVC controller defines the application's routes, and hence is the source of links needed 123 | in the hypermedia. 124 | 125 | NOTE: This guide assumes you already somewhat familiar with Spring MVC. 126 | 127 | [source,java] 128 | ---- 129 | @RestController 130 | class EmployeeController { 131 | 132 | private final EmployeeRepository repository; 133 | private final EmployeeRepresentationModelAssembler assembler; 134 | 135 | EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler) { 136 | 137 | this.repository = repository; 138 | this.assembler = assembler; 139 | } 140 | 141 | ... 142 | 143 | } 144 | ---- 145 | 146 | This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` as well as a 147 | `EmployeeRepresentationModelAssembler` and marked as a *REST controller* thanks to the `@RestController` annotation. 148 | 149 | To support `SimpleIdentifiableRepresentationModelAssembler`, the controller needs two things: 150 | 151 | * A route to the collection. By default, it assumes a pluralized, lowercased name (`Employee` -> `/employees`). 152 | * A route to a single entity. By default it assumes the collection's URI + `/{id}`. 153 | 154 | The collection's route is shown below: 155 | 156 | [source,java] 157 | ---- 158 | /** 159 | * Look up all employees, and transform them into a REST collection resource using 160 | * {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through 161 | * Spring Web's {@link ResponseEntity} fluent API. 162 | */ 163 | @GetMapping("/employees") 164 | public ResponseEntity>> findAll() { 165 | return ResponseEntity.ok( 166 | assembler.toCollectionModel(repository.findAll())); 167 | 168 | } 169 | ---- 170 | 171 | It uses the `EmployeeRepresentationModelAssembler` and it's `toCollectionModel(Iterable)` method to turn a collection of 172 | `Employee` objects into a `CollectionModel>`. 173 | 174 | NOTE: `CollectionModel` is Spring HATEOAS's vendor neutral representation of a collection. It has it's 175 | own set of links, separate from the links of each member of the collection. That's why the whole 176 | structure is `CollectionModel>` and not `CollectionModel`. 177 | 178 | To build a single resource, the `/employees/{id}` route is shown below: 179 | 180 | [source,java] 181 | ---- 182 | /** 183 | * Look up a single {@link Employee} and transform it into a REST resource using 184 | * {@link EmployeeRepresentationModelAssembler#toEntityModel(Object)}. Then return it through 185 | * Spring Web's {@link ResponseEntity} fluent API. 186 | * 187 | * @param id 188 | */ 189 | @GetMapping("/employees/{id}") 190 | public ResponseEntity> findOne(@PathVariable long id) { 191 | return this.repository.findById(id) // 192 | .map(this.assembler::toModel) // 193 | .map(ResponseEntity::ok) // 194 | .orElse(ResponseEntity.notFound().build()); 195 | } 196 | ---- 197 | 198 | Again, the `EmployeeRepresentationModelAssembler` is used to convert a single `Employee` into a `EntityModel` 199 | through its `toEntityModel(Employee)` method. 200 | 201 | == Customizing the Output 202 | 203 | What's not shown in this example is that the `EmployeeRepresentationModelAssembler` comes with overrides. 204 | 205 | * `setBasePath(/* base */)` would inject a prefix into every link built in the hypermedia. 206 | * `addLinks(EntityModel)` and `addLinks(CollectionModel)` allows you to override/augment the default links assigned to every resource. 207 | * `getCollectionLinkBuilder()` lets you override the convention of how the whole route is built up. 208 | 209 | == Testing Hypermedia 210 | 211 | Nothing is complete without testing. Thanks to Spring Boot, it's easier than ever to test a Spring MVC controller, 212 | including the generated hypermedia. 213 | 214 | The following is a bare bones "slice" test case: 215 | 216 | [source,java] 217 | ---- 218 | @RunWith(SpringRunner.class) 219 | @WebMvcTest(EmployeeController.class) 220 | @Import({EmployeeRepresentationModelAssembler.class}) 221 | public class EmployeeControllerTests { 222 | 223 | @Autowired 224 | private MockMvc mvc; 225 | 226 | @MockBean 227 | private EmployeeRepository repository; 228 | 229 | ... 230 | } 231 | ---- 232 | 233 | * `@RunWith(SpringRunner.class)` is needed to leverage Spring Boot's test annotations with JUnit. 234 | * `@WebMvcTest(EmployeeController.class)` confines Spring Boot to only autoconfiguring Spring MVC components, and _only_ 235 | this one controller, making it a very precise test case. 236 | * `@Import({EmployeeRepresentationModelAssembler.class})` pulls in one extra Spring component that would be ignored by `@WebMvcTest`. 237 | * `@Autowired MockMvc` gives us a handle on a Spring Mock tester. 238 | * `@MockBean` flags `EmployeeRepositor` as a test collaborator. 239 | 240 | With this structure, we can start crafting a test case! 241 | 242 | [source,java] 243 | ---- 244 | @Test 245 | public void getShouldFetchAHalDocument() throws Exception { 246 | 247 | given(repository.findAll()).willReturn( 248 | Arrays.asList( 249 | new Employee(1L,"Frodo", "Baggins", "ring bearer"), 250 | new Employee(2L,"Bilbo", "Baggins", "burglar"))); 251 | 252 | mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) 253 | .andDo(print()) 254 | .andExpect(status().isOk()) 255 | .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) 256 | .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) 257 | ... 258 | } 259 | ---- 260 | 261 | * At first, the test case uses Mockito's `given()` method to define the "given"s of the test. 262 | * Next, it uses Spring Mock MVC's `mvc` to `perform()` a *GET /employees* call with an accept header of HAL's media type. 263 | * As a courtesy, it uses the `.andDo(print())` to give us a complete print out of the whole thing on the console. 264 | * Finally, it chains a whole series of assertions. 265 | ** Verify HTTP status is *200 OK*. 266 | ** Verify the response *Content-Type* header is also HAL's media type. 267 | ** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`. 268 | 269 | The rest of the assertions are not shown above, but you can read it in the source code. 270 | 271 | NOTE: This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS 272 | test cases for more examples. 273 | 274 | For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example]. -------------------------------------------------------------------------------- /basics/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-basics 8 | Spring HATEOAS - Examples - Basics 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-maven-plugin 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /basics/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.stereotype.Component; 21 | 22 | /** 23 | * Pre-load some data using a Spring Boot {@link CommandLineRunner}. 24 | * 25 | * @author Greg Turnquist 26 | */ 27 | @Component 28 | class DatabaseLoader { 29 | 30 | /** 31 | * Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run only after the app 32 | * is operational, the database will be up. 33 | * 34 | * @param repository 35 | */ 36 | @Bean 37 | CommandLineRunner init(EmployeeRepository repository) { 38 | 39 | return args -> { 40 | repository.save(new Employee("Frodo", "Baggins", "ring bearer")); 41 | repository.save(new Employee("Bilbo", "Baggins", "burglar")); 42 | }; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /basics/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.AccessLevel; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Data; 21 | import lombok.NoArgsConstructor; 22 | 23 | import java.util.Optional; 24 | 25 | import javax.persistence.Entity; 26 | import javax.persistence.GeneratedValue; 27 | import javax.persistence.Id; 28 | 29 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 30 | 31 | /** 32 | * Domain object representing a company employee. Project Lombok keeps actual code at a minimum. {@code @Data} - 33 | * Generates getters, setters, toString, hash, and equals functions {@code @Entity} - JPA annotation to flag this class 34 | * for DB persistence {@code @NoArgsConstructor} - Create a constructor with no args to support JPA 35 | * {@code @AllArgsConstructor} - Create a constructor with all args to support testing 36 | * {@code @JsonIgnoreProperties(ignoreUnknow=true)} When converting JSON to Java, ignore any unrecognized attributes. 37 | * This is critical for REST because it encourages adding new fields in later versions that won't break. It also allows 38 | * things like _links to be ignore as well, meaning HAL documents can be fetched and later posted to the server without 39 | * adjustment. 40 | * 41 | * @author Greg Turnquist 42 | */ 43 | @Data 44 | @Entity 45 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 46 | @AllArgsConstructor 47 | @JsonIgnoreProperties(ignoreUnknown = true) 48 | class Employee { 49 | 50 | @Id @GeneratedValue private Long id; 51 | private String firstName; 52 | private String lastName; 53 | private String role; 54 | 55 | /** 56 | * Useful constructor when id is not yet known. 57 | * 58 | * @param firstName 59 | * @param lastName 60 | * @param role 61 | */ 62 | Employee(String firstName, String lastName, String role) { 63 | 64 | this.firstName = firstName; 65 | this.lastName = lastName; 66 | this.role = role; 67 | } 68 | 69 | public Optional getId() { 70 | return Optional.ofNullable(this.id); 71 | } 72 | 73 | /** 74 | * This method will create another piece of data in the REST resource representation. These types of methods are key 75 | * in supporting backward compatibility. By NOT removing old fields, and instead replacing them with methods like 76 | * this, an API can evolve without breaking old clients. Because of {@code @JsonIgnoreProperties} settings above, this 77 | * attribute will be ignore if sent back to the server, allowing API evolution. 78 | * 79 | * @return 80 | */ 81 | public String getFullName() { 82 | return firstName + " " + lastName; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /basics/src/main/java/org/springframework/hateoas/examples/EmployeeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.hateoas.CollectionModel; 19 | import org.springframework.hateoas.EntityModel; 20 | import org.springframework.http.ResponseEntity; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | /** 26 | * Spring Web {@link RestController} used to generate a REST API. Works by injecting an {@link EmployeeRepository} and 27 | * an {@link EmployeeRepresentationModelAssembler} in the constructor, both of which are used to retrieve data from the 28 | * database, and assemble a REST resource. 29 | * 30 | * @author Greg Turnquist 31 | */ 32 | @RestController 33 | class EmployeeController { 34 | 35 | private final EmployeeRepository repository; 36 | private final EmployeeRepresentationModelAssembler assembler; 37 | 38 | EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler) { 39 | 40 | this.repository = repository; 41 | this.assembler = assembler; 42 | } 43 | 44 | /** 45 | * Look up all employees, and transform them into a REST collection resource using 46 | * {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through Spring Web's 47 | * {@link ResponseEntity} fluent API. 48 | */ 49 | @GetMapping("/employees") 50 | public ResponseEntity>> findAll() { 51 | 52 | return ResponseEntity.ok( // 53 | this.assembler.toCollectionModel(this.repository.findAll())); 54 | 55 | } 56 | 57 | /** 58 | * Look up a single {@link Employee} and transform it into a REST resource using 59 | * {@link EmployeeRepresentationModelAssembler#toModel(Object)}. Then return it through Spring Web's 60 | * {@link ResponseEntity} fluent API. 61 | * 62 | * @param id 63 | */ 64 | @GetMapping("/employees/{id}") 65 | public ResponseEntity> findOne(@PathVariable long id) { 66 | 67 | return this.repository.findById(id) // 68 | .map(this.assembler::toModel) // 69 | .map(ResponseEntity::ok) // 70 | .orElse(ResponseEntity.notFound().build()); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /basics/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.data.repository.CrudRepository; 19 | 20 | /** 21 | * A simple Spring Data {@link CrudRepository} for storing {@link Employee}s. 22 | * 23 | * @author Greg Turnquist 24 | */ 25 | interface EmployeeRepository extends CrudRepository {} 26 | -------------------------------------------------------------------------------- /basics/src/main/java/org/springframework/hateoas/examples/EmployeeRepresentationModelAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler; 19 | import org.springframework.stereotype.Component; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @Component 25 | class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentationModelAssembler { 26 | 27 | /** 28 | * Link the {@link Employee} domain type to the {@link EmployeeController} using this 29 | * {@link SimpleIdentifiableRepresentationModelAssembler} in order to generate both 30 | * {@link org.springframework.hateoas.EntityModel} and {@link org.springframework.hateoas.CollectionModel}. 31 | */ 32 | EmployeeRepresentationModelAssembler() { 33 | super(EmployeeController.class); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /basics/src/main/java/org/springframework/hateoas/examples/SpringHateoasBasicsApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.hateoas.server.core.EvoInflectorLinkRelationProvider; 23 | 24 | /** 25 | * @author Greg Turnquist 26 | */ 27 | @SpringBootApplication 28 | public class SpringHateoasBasicsApplication { 29 | 30 | public static void main(String... args) { 31 | SpringApplication.run(SpringHateoasBasicsApplication.class); 32 | } 33 | 34 | /** 35 | * Format embedded collections by pluralizing the resource's type. 36 | * 37 | * @return 38 | */ 39 | @Bean 40 | EvoInflectorLinkRelationProvider relProvider() { 41 | return new EvoInflectorLinkRelationProvider(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /basics/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.hamcrest.CoreMatchers.*; 19 | import static org.mockito.BDDMockito.*; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 23 | 24 | import java.util.Arrays; 25 | 26 | import org.junit.Test; 27 | import org.junit.runner.RunWith; 28 | import org.springframework.beans.factory.annotation.Autowired; 29 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 30 | import org.springframework.boot.test.mock.mockito.MockBean; 31 | import org.springframework.context.annotation.Import; 32 | import org.springframework.hateoas.MediaTypes; 33 | import org.springframework.http.HttpHeaders; 34 | import org.springframework.test.context.junit4.SpringRunner; 35 | import org.springframework.test.web.servlet.MockMvc; 36 | 37 | /** 38 | * How to test the hypermedia-based {@link EmployeeController} with everything else mocked out. 39 | * 40 | * @author Greg Turnquist 41 | */ 42 | @RunWith(SpringRunner.class) 43 | @WebMvcTest(EmployeeController.class) 44 | @Import({ EmployeeRepresentationModelAssembler.class }) 45 | public class EmployeeControllerTests { 46 | 47 | @Autowired private MockMvc mvc; 48 | 49 | @MockBean private EmployeeRepository repository; 50 | 51 | @Test 52 | public void getShouldFetchAHalDocument() throws Exception { 53 | 54 | given(repository.findAll()).willReturn( // 55 | Arrays.asList( // 56 | new Employee(1L, "Frodo", "Baggins", "ring bearer"), // 57 | new Employee(2L, "Bilbo", "Baggins", "burglar"))); 58 | 59 | mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) // 60 | .andDo(print()) // 61 | .andExpect(status().isOk()) // 62 | .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) 63 | .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) 64 | .andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo"))) 65 | .andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins"))) 66 | .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) 67 | .andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1"))) 68 | .andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees"))) 69 | .andExpect(jsonPath("$._embedded.employees[1].id", is(2))) 70 | .andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo"))) 71 | .andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins"))) 72 | .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) 73 | .andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2"))) 74 | .andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees"))) 75 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/employees"))) // 76 | .andReturn(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /ci/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | MAVEN_OPTS="-Duser.name=jenkins -Duser.home=/tmp/jenkins-home" ./mvnw -P${PROFILE} clean dependency:list test -Dsort -B 6 | -------------------------------------------------------------------------------- /commons/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | commons 8 | Spring HATEOAS - Examples - Commons 9 | Components that may eventually join Spring HATEOAS 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /commons/src/main/java/org/springframework/hateoas/SimpleIdentifiableRepresentationModelAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import lombok.Getter; 21 | import lombok.Setter; 22 | 23 | import java.lang.reflect.Field; 24 | 25 | import org.springframework.core.GenericTypeResolver; 26 | import org.springframework.hateoas.server.LinkBuilder; 27 | import org.springframework.hateoas.server.LinkRelationProvider; 28 | import org.springframework.hateoas.server.SimpleRepresentationModelAssembler; 29 | import org.springframework.hateoas.server.core.EvoInflectorLinkRelationProvider; 30 | import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; 31 | import org.springframework.util.ReflectionUtils; 32 | 33 | /** 34 | * A {@link SimpleRepresentationModelAssembler} that mixes together a Spring web controller and a 35 | * {@link LinkRelationProvider} to build links upon a certain strategy. 36 | * 37 | * @author Greg Turnquist 38 | */ 39 | public class SimpleIdentifiableRepresentationModelAssembler implements SimpleRepresentationModelAssembler { 40 | 41 | /** 42 | * The Spring MVC class for the object from which links will be built. 43 | */ 44 | private final Class controllerClass; 45 | 46 | /** 47 | * A {@link LinkRelationProvider} to look up names of links as options for resource paths. 48 | */ 49 | @Getter private final LinkRelationProvider relProvider; 50 | 51 | /** 52 | * A {@link Class} depicting the object's type. 53 | */ 54 | @Getter private final Class resourceType; 55 | 56 | /** 57 | * Default base path as empty. 58 | */ 59 | @Getter @Setter private String basePath = ""; 60 | 61 | /** 62 | * Default a assembler based on Spring MVC controller, resource type, and {@link LinkRelationProvider}. With this 63 | * combination of information, resources can be defined. 64 | * 65 | * @see #setBasePath(String) to adjust base path to something like "/api"/ 66 | * @param controllerClass - Spring MVC controller to base links off of 67 | * @param relProvider 68 | */ 69 | public SimpleIdentifiableRepresentationModelAssembler(Class controllerClass, LinkRelationProvider relProvider) { 70 | 71 | this.controllerClass = controllerClass; 72 | this.relProvider = relProvider; 73 | 74 | // Find the "T" type contained in "T extends Identifiable", e.g. 75 | // SimpleIdentifiableRepresentationModelAssembler -> User 76 | this.resourceType = GenericTypeResolver.resolveTypeArgument(this.getClass(), 77 | SimpleIdentifiableRepresentationModelAssembler.class); 78 | } 79 | 80 | /** 81 | * Alternate constructor that falls back to {@link EvoInflectorLinkRelationProvider}. 82 | * 83 | * @param controllerClass 84 | */ 85 | public SimpleIdentifiableRepresentationModelAssembler(Class controllerClass) { 86 | this(controllerClass, new EvoInflectorLinkRelationProvider()); 87 | } 88 | 89 | /** 90 | * Add single item self link based on the object and link back to aggregate root of the {@literal T} domain type using 91 | * {@link LinkRelationProvider#getCollectionResourceRelFor(Class)}}. 92 | * 93 | * @param resource 94 | */ 95 | public void addLinks(EntityModel resource) { 96 | 97 | resource.add(getCollectionLinkBuilder().slash(getId(resource)).withSelfRel()); 98 | resource.add(getCollectionLinkBuilder().withRel(this.relProvider.getCollectionResourceRelFor(this.resourceType))); 99 | } 100 | 101 | private Object getId(EntityModel resource) { 102 | 103 | Field id = ReflectionUtils.findField(this.resourceType, "id"); 104 | ReflectionUtils.makeAccessible(id); 105 | 106 | return ReflectionUtils.getField(id, resource.getContent()); 107 | } 108 | 109 | /** 110 | * Add a self link to the aggregate root. 111 | * 112 | * @param resources 113 | */ 114 | public void addLinks(CollectionModel> resources) { 115 | resources.add(getCollectionLinkBuilder().withSelfRel()); 116 | } 117 | 118 | /** 119 | * Build up a URI for the collection using the Spring web controller followed by the resource type transformed by the 120 | * {@link LinkRelationProvider}. Assumption is that an {@literal EmployeeController} serving up {@literal Employee} 121 | * objects will be serving resources at {@code /employees} and {@code /employees/1}. If this is not the case, simply 122 | * override this method in your concrete instance, or resort to overriding {@link #addLinks(EntityModel)} and 123 | * {@link #addLinks(CollectionModel)} where you have full control over exactly what links are put in the individual 124 | * and collection resources. 125 | * 126 | * @return 127 | */ 128 | protected LinkBuilder getCollectionLinkBuilder() { 129 | 130 | WebMvcLinkBuilder linkBuilder = linkTo(this.controllerClass); 131 | 132 | for (String pathComponent : (getPrefix() + this.relProvider.getCollectionResourceRelFor(this.resourceType)) 133 | .split("/")) { 134 | if (!pathComponent.isEmpty()) { 135 | linkBuilder = linkBuilder.slash(pathComponent); 136 | } 137 | } 138 | 139 | return linkBuilder; 140 | } 141 | 142 | /** 143 | * Provide opportunity to override the base path for the URI. 144 | */ 145 | private String getPrefix() { 146 | return getBasePath().isEmpty() ? "" : getBasePath() + "/"; 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /hypermedia/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-hypermedia 8 | Spring HATEOAS - Examples - Hypermedia 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-maven-plugin 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import java.util.Arrays; 19 | 20 | import org.springframework.boot.CommandLineRunner; 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.stereotype.Component; 23 | 24 | /** 25 | * @author Greg Turnquist 26 | */ 27 | @Component 28 | class DatabaseLoader { 29 | 30 | @Bean 31 | CommandLineRunner initDatabase(EmployeeRepository employeeRepository, ManagerRepository managerRepository) { 32 | return args -> { 33 | 34 | /* 35 | * Gather Gandalf's team 36 | */ 37 | Manager gandalf = managerRepository.save(new Manager("Gandalf")); 38 | 39 | Employee frodo = employeeRepository.save(new Employee("Frodo", "ring bearer", gandalf)); 40 | Employee bilbo = employeeRepository.save(new Employee("Bilbo", "burglar", gandalf)); 41 | 42 | gandalf.setEmployees(Arrays.asList(frodo, bilbo)); 43 | managerRepository.save(gandalf); 44 | 45 | /* 46 | * Put together Saruman's team 47 | */ 48 | Manager saruman = managerRepository.save(new Manager("Saruman")); 49 | 50 | Employee sam = employeeRepository.save(new Employee("Sam", "gardener", saruman)); 51 | 52 | saruman.setEmployees(Arrays.asList(sam)); 53 | 54 | managerRepository.save(saruman); 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | 21 | import java.util.Optional; 22 | 23 | import javax.persistence.Entity; 24 | import javax.persistence.GeneratedValue; 25 | import javax.persistence.Id; 26 | import javax.persistence.OneToOne; 27 | 28 | import com.fasterxml.jackson.annotation.JsonIgnore; 29 | 30 | /** 31 | * @author Greg Turnquist 32 | */ 33 | @Data 34 | @Entity 35 | @NoArgsConstructor 36 | class Employee { 37 | 38 | @Id @GeneratedValue private Long id; 39 | private String name; 40 | private String role; 41 | 42 | /** 43 | * To break the recursive, bi-directional relationship, don't serialize {@literal manager}. 44 | */ 45 | @JsonIgnore @OneToOne private Manager manager; 46 | 47 | Employee(String name, String role, Manager manager) { 48 | 49 | this.name = name; 50 | this.role = role; 51 | this.manager = manager; 52 | } 53 | 54 | public Optional getId() { 55 | return Optional.ofNullable(this.id); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import java.util.stream.Collectors; 21 | import java.util.stream.StreamSupport; 22 | 23 | import org.springframework.hateoas.CollectionModel; 24 | import org.springframework.hateoas.EntityModel; 25 | import org.springframework.hateoas.Links; 26 | import org.springframework.http.ResponseEntity; 27 | import org.springframework.web.bind.annotation.GetMapping; 28 | import org.springframework.web.bind.annotation.PathVariable; 29 | import org.springframework.web.bind.annotation.RestController; 30 | 31 | /** 32 | * @author Greg Turnquist 33 | */ 34 | @RestController 35 | class EmployeeController { 36 | 37 | private final EmployeeRepository repository; 38 | private final EmployeeRepresentationModelAssembler assembler; 39 | private final EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler; 40 | 41 | EmployeeController(EmployeeRepository repository, EmployeeRepresentationModelAssembler assembler, 42 | EmployeeWithManagerResourceAssembler employeeWithManagerResourceAssembler) { 43 | 44 | this.repository = repository; 45 | this.assembler = assembler; 46 | this.employeeWithManagerResourceAssembler = employeeWithManagerResourceAssembler; 47 | } 48 | 49 | /** 50 | * Look up all employees, and transform them into a REST collection resource using 51 | * {@link EmployeeRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through Spring Web's 52 | * {@link ResponseEntity} fluent API. 53 | */ 54 | @GetMapping("/employees") 55 | public ResponseEntity>> findAll() { 56 | 57 | return ResponseEntity.ok(assembler.toCollectionModel(repository.findAll())); 58 | 59 | } 60 | 61 | /** 62 | * Look up a single {@link Employee} and transform it into a REST resource using 63 | * {@link EmployeeRepresentationModelAssembler#toModel(Object)}. Then return it through Spring Web's 64 | * {@link ResponseEntity} fluent API. 65 | * 66 | * @param id 67 | */ 68 | @GetMapping("/employees/{id}") 69 | public ResponseEntity> findOne(@PathVariable long id) { 70 | 71 | return repository.findById(id) // 72 | .map(assembler::toModel) // 73 | .map(ResponseEntity::ok) // 74 | .orElse(ResponseEntity.notFound().build()); 75 | } 76 | 77 | /** 78 | * Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link. 79 | * 80 | * @param id 81 | * @return 82 | */ 83 | @GetMapping("/managers/{id}/employees") 84 | public ResponseEntity>> findEmployees(@PathVariable long id) { 85 | 86 | CollectionModel> collectionModel = assembler 87 | .toCollectionModel(repository.findByManagerId(id)); 88 | 89 | Links newLinks = collectionModel.getLinks().merge(Links.MergeMode.REPLACE_BY_REL, 90 | linkTo(methodOn(EmployeeController.class).findEmployees(id)).withSelfRel()); 91 | 92 | return ResponseEntity.ok(CollectionModel.of(collectionModel.getContent(), newLinks)); 93 | } 94 | 95 | @GetMapping("/employees/detailed") 96 | public ResponseEntity>> findAllDetailedEmployees() { 97 | 98 | return ResponseEntity.ok( // 99 | employeeWithManagerResourceAssembler.toCollectionModel( // 100 | StreamSupport.stream(repository.findAll().spliterator(), false) // 101 | .map(EmployeeWithManager::new) // 102 | .collect(Collectors.toList()))); 103 | } 104 | 105 | @GetMapping("/employees/{id}/detailed") 106 | public ResponseEntity> findDetailedEmployee(@PathVariable Long id) { 107 | 108 | return repository.findById(id) // 109 | .map(EmployeeWithManager::new) // 110 | .map(employeeWithManagerResourceAssembler::toModel) // 111 | .map(ResponseEntity::ok) // 112 | .orElse(ResponseEntity.notFound().build()); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import java.util.List; 19 | 20 | import org.springframework.data.repository.CrudRepository; 21 | 22 | /** 23 | * @author Greg Turnquist 24 | */ 25 | interface EmployeeRepository extends CrudRepository { 26 | 27 | List findByManagerId(Long id); 28 | 29 | } 30 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeRepresentationModelAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import org.springframework.hateoas.CollectionModel; 21 | import org.springframework.hateoas.EntityModel; 22 | import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler; 23 | import org.springframework.stereotype.Component; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | @Component 29 | class EmployeeRepresentationModelAssembler extends SimpleIdentifiableRepresentationModelAssembler { 30 | 31 | EmployeeRepresentationModelAssembler() { 32 | super(EmployeeController.class); 33 | } 34 | 35 | /** 36 | * Define links to add to every {@link EntityModel}. 37 | * 38 | * @param resource 39 | */ 40 | @Override 41 | public void addLinks(EntityModel resource) { 42 | 43 | /** 44 | * Add some custom links to the default ones provided. NOTE: To replace default links, don't invoke 45 | * {@literal super.addLinks()}. 46 | */ 47 | super.addLinks(resource); 48 | 49 | resource.getContent().getId() // 50 | .ifPresent(id -> { // 51 | // Add additional links 52 | resource.add(linkTo(methodOn(ManagerController.class).findManager(id)).withRel("manager")); 53 | resource.add(linkTo(methodOn(EmployeeController.class).findDetailedEmployee(id)).withRel("detailed")); 54 | 55 | // Maintain a legacy link to support older clients not yet adjusted to the switch from "supervisor" to 56 | // "manager". 57 | resource.add(linkTo(methodOn(SupervisorController.class).findOne(id)).withRel("supervisor")); 58 | }); 59 | } 60 | 61 | /** 62 | * Define links to add to {@link CollectionModel} collection. 63 | * 64 | * @param resources 65 | */ 66 | @Override 67 | public void addLinks(CollectionModel> resources) { 68 | 69 | super.addLinks(resources); 70 | 71 | resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); 72 | resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); 73 | resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Value; 19 | 20 | import com.fasterxml.jackson.annotation.JsonIgnore; 21 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 22 | 23 | /** 24 | * Class defined purely for hosting an "detailed" point of view for REST. 25 | * 26 | * @author Greg Turnquist 27 | */ 28 | @Value 29 | @JsonPropertyOrder({ "id", "name", "role", "manager" }) 30 | public class EmployeeWithManager { 31 | 32 | @JsonIgnore private final Employee employee; 33 | 34 | public Long getId() { 35 | 36 | return this.employee.getId() // 37 | .orElseThrow(() -> new RuntimeException("Couldn't find anything.")); 38 | } 39 | 40 | public String getName() { 41 | return this.employee.getName(); 42 | } 43 | 44 | public String getRole() { 45 | return this.employee.getRole(); 46 | } 47 | 48 | public String getManager() { 49 | return this.employee.getManager().getName(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/EmployeeWithManagerResourceAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import org.springframework.hateoas.CollectionModel; 21 | import org.springframework.hateoas.EntityModel; 22 | import org.springframework.hateoas.server.SimpleRepresentationModelAssembler; 23 | import org.springframework.stereotype.Component; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | @Component 29 | class EmployeeWithManagerResourceAssembler implements SimpleRepresentationModelAssembler { 30 | 31 | /** 32 | * Define links to add to every individual {@link EntityModel}. 33 | * 34 | * @param resource 35 | */ 36 | @Override 37 | public void addLinks(EntityModel resource) { 38 | 39 | resource.add( 40 | linkTo(methodOn(EmployeeController.class).findDetailedEmployee(resource.getContent().getId())).withSelfRel()); 41 | resource.add(linkTo(methodOn(EmployeeController.class).findOne(resource.getContent().getId())).withRel("summary")); 42 | resource.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); 43 | } 44 | 45 | /** 46 | * Define links to add to the {@link CollectionModel} collection. 47 | * 48 | * @param resources 49 | */ 50 | @Override 51 | public void addLinks(CollectionModel> resources) { 52 | 53 | resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withSelfRel()); 54 | resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); 55 | resources.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); 56 | resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/Manager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Data; 19 | import lombok.NoArgsConstructor; 20 | 21 | import java.util.ArrayList; 22 | import java.util.List; 23 | import java.util.Optional; 24 | 25 | import javax.persistence.Entity; 26 | import javax.persistence.GeneratedValue; 27 | import javax.persistence.Id; 28 | import javax.persistence.OneToMany; 29 | 30 | import com.fasterxml.jackson.annotation.JsonIgnore; 31 | 32 | /** 33 | * @author Greg Turnquist 34 | */ 35 | @Data 36 | @Entity 37 | @NoArgsConstructor 38 | class Manager { 39 | 40 | @Id @GeneratedValue private Long id; 41 | private String name; 42 | 43 | /** 44 | * To break the recursive, bi-directional interface, don't serialize {@literal employees}. 45 | */ 46 | @JsonIgnore // 47 | @OneToMany(mappedBy = "manager") // 48 | private List employees = new ArrayList<>(); 49 | 50 | Manager(String name) { 51 | this.name = name; 52 | } 53 | 54 | public Optional getId() { 55 | return Optional.ofNullable(this.id); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.hateoas.CollectionModel; 19 | import org.springframework.hateoas.EntityModel; 20 | import org.springframework.http.ResponseEntity; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.PathVariable; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | @RestController 29 | class ManagerController { 30 | 31 | private final ManagerRepository repository; 32 | private final ManagerRepresentationModelAssembler assembler; 33 | 34 | ManagerController(ManagerRepository repository, ManagerRepresentationModelAssembler assembler) { 35 | 36 | this.repository = repository; 37 | this.assembler = assembler; 38 | } 39 | 40 | /** 41 | * Look up all managers, and transform them into a REST collection resource using 42 | * {@link ManagerRepresentationModelAssembler#toCollectionModel(Iterable)}. Then return them through Spring Web's 43 | * {@link ResponseEntity} fluent API. 44 | */ 45 | @GetMapping("/managers") 46 | ResponseEntity>> findAll() { 47 | 48 | return ResponseEntity.ok( // 49 | assembler.toCollectionModel(repository.findAll())); 50 | 51 | } 52 | 53 | /** 54 | * Look up a single {@link Manager} and transform it into a REST resource using 55 | * {@link ManagerRepresentationModelAssembler#toModel(Object)}. Then return it through Spring Web's 56 | * {@link ResponseEntity} fluent API. 57 | * 58 | * @param id 59 | */ 60 | @GetMapping("/managers/{id}") 61 | ResponseEntity> findOne(@PathVariable long id) { 62 | 63 | return repository.findById(id) // 64 | .map(assembler::toModel) // 65 | .map(ResponseEntity::ok) // 66 | .orElse(ResponseEntity.notFound().build()); 67 | } 68 | 69 | /** 70 | * Find an {@link Employee}'s {@link Manager} based upon employee id. Turn it into a context-based link. 71 | * 72 | * @param id 73 | * @return 74 | */ 75 | @GetMapping("/employees/{id}/manager") 76 | ResponseEntity> findManager(@PathVariable long id) { 77 | 78 | return ResponseEntity.ok( // 79 | assembler.toModel(repository.findByEmployeesId(id))); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.data.repository.CrudRepository; 19 | 20 | /** 21 | * @author Greg Turnquist 22 | */ 23 | interface ManagerRepository extends CrudRepository { 24 | 25 | /** 26 | * Navigate through the JPA relationship to find a {@link Manager} based on an {@link Employee}'s {@literal id}. 27 | * 28 | * @param id 29 | * @return 30 | */ 31 | Manager findByEmployeesId(Long id); 32 | } 33 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/ManagerRepresentationModelAssembler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import org.springframework.hateoas.CollectionModel; 21 | import org.springframework.hateoas.EntityModel; 22 | import org.springframework.hateoas.SimpleIdentifiableRepresentationModelAssembler; 23 | import org.springframework.stereotype.Component; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | @Component 29 | class ManagerRepresentationModelAssembler extends SimpleIdentifiableRepresentationModelAssembler { 30 | 31 | ManagerRepresentationModelAssembler() { 32 | super(ManagerController.class); 33 | } 34 | 35 | /** 36 | * Retain default links provided by {@link SimpleIdentifiableRepresentationModelAssembler}, but add extra ones to each 37 | * {@link Manager}. 38 | * 39 | * @param resource 40 | */ 41 | @Override 42 | public void addLinks(EntityModel resource) { 43 | /** 44 | * Retain default links. 45 | */ 46 | super.addLinks(resource); 47 | 48 | resource.getContent().getId() // 49 | .ifPresent(id -> { // 50 | // Add custom link to find all managed employees 51 | resource.add(linkTo(methodOn(EmployeeController.class).findEmployees(id)).withRel("employees")); 52 | }); 53 | } 54 | 55 | /** 56 | * Retain default links for the entire collection, but add extra custom links for the {@link Manager} collection. 57 | * 58 | * @param resources 59 | */ 60 | @Override 61 | public void addLinks(CollectionModel> resources) { 62 | 63 | super.addLinks(resources); 64 | 65 | resources.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); 66 | resources.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); 67 | resources.add(linkTo(methodOn(RootController.class).root()).withRel("root")); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/RootController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import org.springframework.hateoas.RepresentationModel; 21 | import org.springframework.http.ResponseEntity; 22 | import org.springframework.web.bind.annotation.GetMapping; 23 | import org.springframework.web.bind.annotation.RestController; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | @RestController 29 | class RootController { 30 | 31 | @GetMapping("/") 32 | ResponseEntity root() { 33 | 34 | RepresentationModel model = new RepresentationModel(); 35 | 36 | model.add(linkTo(methodOn(RootController.class).root()).withSelfRel()); 37 | model.add(linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")); 38 | model.add(linkTo(methodOn(EmployeeController.class).findAllDetailedEmployees()).withRel("detailedEmployees")); 39 | model.add(linkTo(methodOn(ManagerController.class).findAll()).withRel("managers")); 40 | 41 | return ResponseEntity.ok(model); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/SpringHateoasHypermediaApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.SpringApplication; 19 | import org.springframework.boot.autoconfigure.SpringBootApplication; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | @SpringBootApplication 25 | public class SpringHateoasHypermediaApplication { 26 | 27 | public static void main(String[] args) { 28 | SpringApplication.run(SpringHateoasHypermediaApplication.class, args); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/Supervisor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.Value; 19 | 20 | import java.util.List; 21 | import java.util.stream.Collectors; 22 | 23 | import com.fasterxml.jackson.annotation.JsonIgnore; 24 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 25 | 26 | /** 27 | * Legacy representation. Contains older format of data. Fewer links because hypermedia at the time was an after 28 | * thought. 29 | * 30 | * @author Greg Turnquist 31 | */ 32 | @Value 33 | @JsonPropertyOrder({ "id", "name", "employees" }) 34 | class Supervisor { 35 | 36 | @JsonIgnore private final Manager manager; 37 | 38 | public Long getId() { 39 | 40 | return this.manager.getId() // 41 | .orElseThrow(() -> new RuntimeException("Couldn't find anything")); 42 | } 43 | 44 | public String getName() { 45 | return this.manager.getName(); 46 | } 47 | 48 | public List getEmployees() { 49 | 50 | return manager.getEmployees().stream() // 51 | .map(employee -> employee.getName() + "::" + employee.getRole()) // 52 | .collect(Collectors.toList()); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /hypermedia/src/main/java/org/springframework/hateoas/examples/SupervisorController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.hateoas.EntityModel; 19 | import org.springframework.http.ResponseEntity; 20 | import org.springframework.web.bind.annotation.GetMapping; 21 | import org.springframework.web.bind.annotation.PathVariable; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | /** 25 | * Represent an older controller that has since been replaced with {@link ManagerController}. This controller is used to 26 | * provide legacy routes, i.e. backwards compatibility. 27 | * 28 | * @author Greg Turnquist 29 | */ 30 | @RestController 31 | public class SupervisorController { 32 | 33 | private final ManagerController controller; 34 | 35 | public SupervisorController(ManagerController controller) { 36 | this.controller = controller; 37 | } 38 | 39 | @GetMapping("/supervisors/{id}") 40 | public ResponseEntity> findOne(@PathVariable Long id) { 41 | 42 | EntityModel managerResource = controller.findOne(id).getBody(); 43 | 44 | EntityModel supervisorResource = EntityModel.of( // 45 | new Supervisor(managerResource.getContent()), // 46 | managerResource.getLinks()); 47 | 48 | return ResponseEntity.ok(supervisorResource); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven2 Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # 58 | # Look for the Apple JDKs first to preserve the existing behaviour, and then look 59 | # for the new JDKs provided by Oracle. 60 | # 61 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK ] ; then 62 | # 63 | # Apple JDKs 64 | # 65 | export JAVA_HOME=/System/Library/Frameworks/JavaVM.framework/Versions/CurrentJDK/Home 66 | fi 67 | 68 | if [ -z "$JAVA_HOME" ] && [ -L /System/Library/Java/JavaVirtualMachines/CurrentJDK ] ; then 69 | # 70 | # Apple JDKs 71 | # 72 | export JAVA_HOME=/System/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 73 | fi 74 | 75 | if [ -z "$JAVA_HOME" ] && [ -L "/Library/Java/JavaVirtualMachines/CurrentJDK" ] ; then 76 | # 77 | # Oracle JDKs 78 | # 79 | export JAVA_HOME=/Library/Java/JavaVirtualMachines/CurrentJDK/Contents/Home 80 | fi 81 | 82 | if [ -z "$JAVA_HOME" ] && [ -x "/usr/libexec/java_home" ]; then 83 | # 84 | # Apple JDKs 85 | # 86 | export JAVA_HOME=`/usr/libexec/java_home` 87 | fi 88 | ;; 89 | esac 90 | 91 | if [ -z "$JAVA_HOME" ] ; then 92 | if [ -r /etc/gentoo-release ] ; then 93 | JAVA_HOME=`java-config --jre-home` 94 | fi 95 | fi 96 | 97 | if [ -z "$M2_HOME" ] ; then 98 | ## resolve links - $0 may be a link to maven's home 99 | PRG="$0" 100 | 101 | # need this for relative symlinks 102 | while [ -h "$PRG" ] ; do 103 | ls=`ls -ld "$PRG"` 104 | link=`expr "$ls" : '.*-> \(.*\)$'` 105 | if expr "$link" : '/.*' > /dev/null; then 106 | PRG="$link" 107 | else 108 | PRG="`dirname "$PRG"`/$link" 109 | fi 110 | done 111 | 112 | saveddir=`pwd` 113 | 114 | M2_HOME=`dirname "$PRG"`/.. 115 | 116 | # make it fully qualified 117 | M2_HOME=`cd "$M2_HOME" && pwd` 118 | 119 | cd "$saveddir" 120 | # echo Using m2 at $M2_HOME 121 | fi 122 | 123 | # For Cygwin, ensure paths are in UNIX format before anything is touched 124 | if $cygwin ; then 125 | [ -n "$M2_HOME" ] && 126 | M2_HOME=`cygpath --unix "$M2_HOME"` 127 | [ -n "$JAVA_HOME" ] && 128 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 129 | [ -n "$CLASSPATH" ] && 130 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 131 | fi 132 | 133 | # For Migwn, ensure paths are in UNIX format before anything is touched 134 | if $mingw ; then 135 | [ -n "$M2_HOME" ] && 136 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 137 | [ -n "$JAVA_HOME" ] && 138 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 139 | # TODO classpath? 140 | fi 141 | 142 | if [ -z "$JAVA_HOME" ]; then 143 | javaExecutable="`which javac`" 144 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 145 | # readlink(1) is not available as standard on Solaris 10. 146 | readLink=`which readlink` 147 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 148 | if $darwin ; then 149 | javaHome="`dirname \"$javaExecutable\"`" 150 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 151 | else 152 | javaExecutable="`readlink -f \"$javaExecutable\"`" 153 | fi 154 | javaHome="`dirname \"$javaExecutable\"`" 155 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 156 | JAVA_HOME="$javaHome" 157 | export JAVA_HOME 158 | fi 159 | fi 160 | fi 161 | 162 | if [ -z "$JAVACMD" ] ; then 163 | if [ -n "$JAVA_HOME" ] ; then 164 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 165 | # IBM's JDK on AIX uses strange locations for the executables 166 | JAVACMD="$JAVA_HOME/jre/sh/java" 167 | else 168 | JAVACMD="$JAVA_HOME/bin/java" 169 | fi 170 | else 171 | JAVACMD="`which java`" 172 | fi 173 | fi 174 | 175 | if [ ! -x "$JAVACMD" ] ; then 176 | echo "Error: JAVA_HOME is not defined correctly." >&2 177 | echo " We cannot execute $JAVACMD" >&2 178 | exit 1 179 | fi 180 | 181 | if [ -z "$JAVA_HOME" ] ; then 182 | echo "Warning: JAVA_HOME environment variable is not set." 183 | fi 184 | 185 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 186 | 187 | # For Cygwin, switch paths to Windows format before running java 188 | if $cygwin; then 189 | [ -n "$M2_HOME" ] && 190 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 191 | [ -n "$JAVA_HOME" ] && 192 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 193 | [ -n "$CLASSPATH" ] && 194 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 195 | fi 196 | 197 | # traverses directory structure from process work directory to filesystem root 198 | # first directory with .mvn subdirectory is considered project base directory 199 | find_maven_basedir() { 200 | local basedir=$(pwd) 201 | local wdir=$(pwd) 202 | while [ "$wdir" != '/' ] ; do 203 | if [ -d "$wdir"/.mvn ] ; then 204 | basedir=$wdir 205 | break 206 | fi 207 | wdir=$(cd "$wdir/.."; pwd) 208 | done 209 | echo "${basedir}" 210 | } 211 | 212 | # concatenates all lines of a file 213 | concat_lines() { 214 | if [ -f "$1" ]; then 215 | echo "$(tr -s '\n' ' ' < "$1")" 216 | fi 217 | } 218 | 219 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-$(find_maven_basedir)} 220 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 221 | 222 | # Provide a "standardized" way to retrieve the CLI args that will 223 | # work with both Windows and non-Windows executions. 224 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 225 | export MAVEN_CMD_LINE_ARGS 226 | 227 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 228 | 229 | exec "$JAVACMD" \ 230 | $MAVEN_OPTS \ 231 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 232 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 233 | ${WRAPPER_LAUNCHER} "$@" 234 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM https://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven2 Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM enable echoing my setting MAVEN_BATCH_ECHO to 'on' 39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 40 | 41 | @REM set %HOME% to equivalent of $HOME 42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 43 | 44 | @REM Execute a user defined script before this one 45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 49 | :skipRcPre 50 | 51 | @setlocal 52 | 53 | set ERROR_CODE=0 54 | 55 | @REM To isolate internal variables from possible post scripts, we use another setlocal 56 | @setlocal 57 | 58 | @REM ==== START VALIDATION ==== 59 | if not "%JAVA_HOME%" == "" goto OkJHome 60 | 61 | echo. 62 | echo Error: JAVA_HOME not found in your environment. >&2 63 | echo Please set the JAVA_HOME variable in your environment to match the >&2 64 | echo location of your Java installation. >&2 65 | echo. 66 | goto error 67 | 68 | :OkJHome 69 | if exist "%JAVA_HOME%\bin\java.exe" goto init 70 | 71 | echo. 72 | echo Error: JAVA_HOME is set to an invalid directory. >&2 73 | echo JAVA_HOME = "%JAVA_HOME%" >&2 74 | echo Please set the JAVA_HOME variable in your environment to match the >&2 75 | echo location of your Java installation. >&2 76 | echo. 77 | goto error 78 | 79 | @REM ==== END VALIDATION ==== 80 | 81 | :init 82 | 83 | set MAVEN_CMD_LINE_ARGS=%* 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | 121 | set WRAPPER_JAR="".\.mvn\wrapper\maven-wrapper.jar"" 122 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 123 | 124 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CMD_LINE_ARGS% 125 | if ERRORLEVEL 1 goto error 126 | goto end 127 | 128 | :error 129 | set ERROR_CODE=1 130 | 131 | :end 132 | @endlocal & set ERROR_CODE=%ERROR_CODE% 133 | 134 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 135 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 136 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 137 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 138 | :skipRcPost 139 | 140 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 141 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 142 | 143 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 144 | 145 | exit /B %ERROR_CODE% -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | org.springframework.hateoas.examples 7 | spring-hateoas-examples 8 | 1.0.0.BUILD-SNAPSHOT 9 | pom 10 | 11 | Spring HATEOAS - Examples 12 | Examples using Spring HATEOAS to build RESTful services 13 | 14 | 2017 15 | 16 | 17 | 18 | Greg L. Turnquist 19 | Pivotal 20 | https://spring.io 21 | 22 | Project Lead 23 | 24 | -6 25 | https://spring.io/team/gturnquist 26 | gturnquist (at) pivotal.io 27 | 28 | 29 | 30 | 31 | 32 | Apache License, Version 2.0 33 | https://www.apache.org/licenses/LICENSE-2.0 34 | 35 | Copyright 2017-2019 the original author or authors. 36 | 37 | Licensed under the Apache License, Version 2.0 (the "License"); 38 | you may not use this file except in compliance with the License. 39 | You may obtain a copy of the License at 40 | 41 | https://www.apache.org/licenses/LICENSE-2.0 42 | 43 | Unless required by applicable law or agreed to in writing, software 44 | distributed under the License is distributed on an "AS IS" BASIS, 45 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 46 | implied. 47 | See the License for the specific language governing permissions and 48 | limitations under the License. 49 | 50 | 51 | 52 | 53 | 54 | org.springframework.boot 55 | spring-boot-starter-parent 56 | 2.3.4.RELEASE 57 | 58 | 59 | 60 | 61 | commons 62 | basics 63 | api-evolution 64 | hypermedia 65 | affordances 66 | simplified 67 | spring-hateoas-and-spring-data-rest 68 | 69 | 70 | 71 | UTF-8 72 | UTF-8 73 | 1.8 74 | 75 | 1.2.2 76 | 77 | 78 | 79 | 80 | 81 | spring52-next 82 | 83 | 5.2.6.BUILD-SNAPSHOT 84 | 85 | 86 | 87 | spring-libs-snapshot 88 | https://repo.spring.io/libs-snapshot 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | gturnquist 98 | Greg Turnquist 99 | gturnquist@pivotal.io 100 | Pivotal Software, Inc. 101 | 102 | Project Lead 103 | 104 | 105 | 106 | 107 | 108 | 109 | org.springframework.boot 110 | spring-boot-starter-hateoas 111 | 112 | 113 | 114 | org.atteo 115 | evo-inflector 116 | ${evo.version} 117 | 118 | 119 | 120 | org.springframework.boot 121 | spring-boot-starter-data-jpa 122 | 123 | 124 | 125 | com.h2database 126 | h2 127 | 128 | 129 | 130 | org.springframework.boot 131 | spring-boot-devtools 132 | 133 | 134 | 135 | org.projectlombok 136 | lombok 137 | 138 | 139 | 140 | 141 | 142 | org.springframework.boot 143 | spring-boot-starter-test 144 | test 145 | 146 | 147 | 148 | 149 | 150 | 151 | maven-surefire-plugin 152 | 153 | false 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | spring-libs-snapshot 162 | https://repo.spring.io/libs-snapshot 163 | 164 | 165 | 166 | 167 | 168 | spring-libs-snapshot 169 | https://repo.spring.io/libs-snapshot 170 | 171 | 172 | 173 | 174 | -------------------------------------------------------------------------------- /security/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-security 8 | Spring HATEOAS - Examples - Security 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-maven-plugin 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /simplified/README.adoc: -------------------------------------------------------------------------------- 1 | = Spring HATEOAS - Basic Example 2 | 3 | This guides shows how to add Spring HATEOAS in the simplest way possible. Like the rest of these examples, it uses a payroll system. 4 | 5 | NOTE: This example uses https://projectlombok.org[Project Lombok] to reduce writing Java code. 6 | 7 | == Defining Your Domain 8 | 9 | The cornerstone of any example is the domain object: 10 | 11 | [source,java] 12 | ---- 13 | @Data 14 | @Entity 15 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 16 | @AllArgsConstructor 17 | class Employee { 18 | 19 | @Id @GeneratedValue 20 | private Long id; 21 | private String firstName; 22 | private String lastName; 23 | private String role; 24 | 25 | ... 26 | } 27 | ---- 28 | 29 | This domain object includes: 30 | 31 | * `@Data` - Lombok annotation to define a mutable value object 32 | * `@Entity` - JPA annotation to make the object storagable in a classic SQL engine (H2 in this example) 33 | * `@NoArgsConstructor(PRIVATE)` - Lombok annotation to create an empty constructor call to appease Jackson, but which is private and not usable to our app's code. 34 | * `@AllArgsConstructor` - Lombok annotation to create an all-arg constructor for certain test scenarios 35 | 36 | == Accessing Data 37 | 38 | To experiment with something realistic, you need to access a real database. This example leverages H2, an embedded JPA datasource. 39 | And while it's not a requirement for Spring HATEOAS, this example uses Spring Data JPA. 40 | 41 | Create a repository like this: 42 | 43 | [source,java] 44 | ---- 45 | interface EmployeeRepository extends CrudRepository { 46 | } 47 | ---- 48 | 49 | This interface extends Spring Data Commons' `CrudRepository`, inheriting a collection of create/replace/update/delete (CRUD) 50 | operations. 51 | 52 | [[converting-entities-to-resources]] 53 | == Converting Entities to Resources 54 | 55 | In REST, the "thing" being linked to is a *resource*. Resources provide both information as well as details on _how_ to 56 | retrieve and update that information. 57 | 58 | Spring HATEOAS defines a generic `EntityModel` container that lets you store any domain object (`Employee` in this example), and 59 | add additional links. 60 | 61 | IMPORTANT: Spring HATEOAS's `Resource` and `Link` classes are *vendor neutral*. HAL is thrown around a lot, being the 62 | default media type, but these classes can be used to render any media type. 63 | 64 | The following Spring MVC controller defines the application's routes, and hence is the source of links needed 65 | in the hypermedia. 66 | 67 | NOTE: This guide assumes you already somewhat familiar with Spring MVC. 68 | 69 | [source,java] 70 | ---- 71 | @RestController 72 | class EmployeeController { 73 | 74 | private final EmployeeRepository repository; 75 | 76 | EmployeeController(EmployeeRepository repository) { 77 | this.repository = repository; 78 | } 79 | 80 | ... 81 | } 82 | ---- 83 | 84 | This piece of code shows how the Spring MVC controller is wired with a copy of the `EmployeeRepository` through 85 | constructor injection and marked as a *REST controller* thanks to the `@RestController` annotation. 86 | 87 | The route for the https://martinfowler.com/bliki/DDD_Aggregate.html[aggregate root] is shown below: 88 | 89 | [source,java] 90 | ---- 91 | /** 92 | * Look up all employees, and transform them into a REST collection resource. 93 | * Then return them through Spring Web's {@link ResponseEntity} fluent API. 94 | */ 95 | @GetMapping("/employees") 96 | ResponseEntity>> findAll() { 97 | 98 | List> employees = StreamSupport.stream(repository.findAll().spliterator(), false) 99 | .map(employee -> EntityModel.of(employee, 100 | linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), 101 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) 102 | .collect(Collectors.toList()); 103 | 104 | return ResponseEntity.ok( 105 | CollectionModel.of(employees, 106 | linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel())); 107 | } 108 | ---- 109 | 110 | It retrieves a collection of `Employee` objects, streams through a Java 8 spliterator, and converts them into a collection 111 | of `EntityModel` objects by using Spring HATEOAS's `linkTo` and `methodOn` helpers to build links. 112 | 113 | * The natural convention with REST endpoints is to serve a *self* link (denoted by the `.withSelfRel()` call). 114 | * It's also useful for any single item resource to include a link back to the aggregate (denoted by the `.withRel("employees")`). 115 | 116 | The whole collection of single item resources is then wrapped in a Spring HATEOAS `Resources` type. 117 | 118 | NOTE: `Resources` is Spring HATEOAS's vendor neutral representation of a collection. It has it's 119 | own set of links, separate from the links of each member of the collection. That's why the whole 120 | structure is `CollectionModel>` and not `CollectionModel`. 121 | 122 | To build a single resource, the `/employees/{id}` route is shown below: 123 | 124 | [source,java] 125 | ---- 126 | /** 127 | * Look up a single {@link Employee} and transform it into a REST resource. Then return it through 128 | * Spring Web's {@link ResponseEntity} fluent API. 129 | * 130 | * @param id 131 | */ 132 | @GetMapping("/employees/{id}") 133 | ResponseEntity> findOne(@PathVariable long id) { 134 | 135 | return repository.findById(id) 136 | .map(employee -> EntityModel.of(employee, 137 | linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), 138 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) 139 | .map(ResponseEntity::ok) 140 | .orElse(ResponseEntity.notFound().build()); 141 | } 142 | ---- 143 | 144 | This code is almost identical. It fetches a single item `Employee` from the database and that wraps up into a 145 | `EntityModel` object with the same links, but that's it. No need to create a `Resources` object since is NOT a 146 | collection. 147 | 148 | IMPORTANT: Does this look like duplicate code found in the aggregate root? Sures it does. That's why Spring HATEOAS 149 | includes the ability to define a `ResourceAssembler`. It lets you define, in one place, all the links for a given 150 | entity type. Then you can reuse it as needed in all relevant controller methods. It's been left out of this section 151 | for the sake of simplicity. 152 | 153 | == Testing Hypermedia 154 | 155 | Nothing is complete without testing. Thanks to Spring Boot, it's easier than ever to test a Spring MVC controller, 156 | including the generated hypermedia. 157 | 158 | The following is a bare bones "slice" test case: 159 | 160 | [source,java] 161 | ---- 162 | @RunWith(SpringRunner.class) 163 | @WebMvcTest(EmployeeController.class) 164 | public class EmployeeControllerTests { 165 | 166 | @Autowired 167 | private MockMvc mvc; 168 | 169 | @MockBean 170 | private EmployeeRepository repository; 171 | 172 | ... 173 | } 174 | ---- 175 | 176 | * `@RunWith(SpringRunner.class)` is needed to leverage Spring Boot's test annotations with JUnit. 177 | * `@WebMvcTest(EmployeeController.class)` confines Spring Boot to only autoconfiguring Spring MVC components, and _only_ 178 | this one controller, making it a very precise test case. 179 | * `@Autowired MockMvc` gives us a handle on a Spring Mock tester. 180 | * `@MockBean` flags `EmployeeRepository` as a test collaborator, since we don't plan on talking to a real database in this test case. 181 | 182 | With this structure, we can start crafting a test case! 183 | 184 | [source,java] 185 | ---- 186 | @Test 187 | public void getShouldFetchAHalDocument() throws Exception { 188 | 189 | given(repository.findAll()).willReturn( 190 | Arrays.asList( 191 | new Employee(1L,"Frodo", "Baggins", "ring bearer"), 192 | new Employee(2L,"Bilbo", "Baggins", "burglar"))); 193 | 194 | mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) 195 | .andDo(print()) 196 | .andExpect(status().isOk()) 197 | .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE)) 198 | .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) 199 | ... 200 | } 201 | ---- 202 | 203 | * At first, the test case uses Mockito's `given()` method to define the "given"s of the test. 204 | * Next, it uses Spring Mock MVC's `mvc` to `perform()` a *GET /employees* call with an accept header of HAL's media type. 205 | * As a courtesy, it uses the `.andDo(print())` to give us a complete print out of the whole thing on the console. 206 | * Finally, it chains a whole series of assertions. 207 | ** Verify HTTP status is *200 OK*. 208 | ** Verify the response *Content-Type* header is also HAL's media type (with UTF-8 flavor). 209 | ** Verify that the JSON Path of *$._embedded.employees[0].id* is `1`. 210 | ** And so forth... 211 | 212 | The rest of the assertions are commented out, but you can read it in the source code. 213 | 214 | NOTE: This is not the only way to assert the results. See Spring Framework reference docs and Spring HATEOAS 215 | test cases for more examples. 216 | 217 | For the next step in Spring HATEOAS, you may wish to read link:../api-evolution[Spring HATEOAS - API Evolution Example]. -------------------------------------------------------------------------------- /simplified/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-simplified 8 | Spring HATEOAS - Examples - Simplified 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | 26 | 27 | 28 | org.springframework.boot 29 | spring-boot-maven-plugin 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /simplified/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.boot.CommandLineRunner; 19 | import org.springframework.context.annotation.Bean; 20 | import org.springframework.stereotype.Component; 21 | 22 | /** 23 | * Pre-load some data using a Spring Boot {@link CommandLineRunner}. 24 | * 25 | * @author Greg Turnquist 26 | */ 27 | @Component 28 | class DatabaseLoader { 29 | 30 | /** 31 | * Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run only after the app 32 | * is operational, the database will be up. 33 | * 34 | * @param repository 35 | */ 36 | @Bean 37 | CommandLineRunner init(EmployeeRepository repository) { 38 | 39 | return args -> { 40 | repository.save(new Employee("Frodo", "Baggins", "ring bearer")); 41 | repository.save(new Employee("Bilbo", "Baggins", "burglar")); 42 | }; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /simplified/src/main/java/org/springframework/hateoas/examples/Employee.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import lombok.AccessLevel; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Data; 21 | import lombok.NoArgsConstructor; 22 | 23 | import javax.persistence.Entity; 24 | import javax.persistence.GeneratedValue; 25 | import javax.persistence.Id; 26 | 27 | /** 28 | * Domain object representing a company employee. Project Lombok keeps actual code at a minimum. {@code @Data} - 29 | * Generates getters, setters, toString, hash, and equals functions {@code @Entity} - JPA annotation to flag this class 30 | * for DB persistence {@code @NoArgsConstructor} - Create a constructor with no args to support JPA 31 | * {@code @AllArgsConstructor} - Create a constructor with all args to support testing 32 | * {@code @JsonIgnoreProperties(ignoreUnknow=true)} When converting JSON to Java, ignore any unrecognized attributes. 33 | * This is critical for REST because it encourages adding new fields in later versions that won't break. It also allows 34 | * things like _links to be ignore as well, meaning HAL documents can be fetched and later posted to the server without 35 | * adjustment. 36 | * 37 | * @author Greg Turnquist 38 | */ 39 | @Data 40 | @Entity 41 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 42 | @AllArgsConstructor 43 | class Employee { 44 | 45 | @Id @GeneratedValue private Long id; 46 | private String firstName; 47 | private String lastName; 48 | private String role; 49 | 50 | /** 51 | * Useful constructor when id is not yet known. 52 | * 53 | * @param firstName 54 | * @param lastName 55 | * @param role 56 | */ 57 | Employee(String firstName, String lastName, String role) { 58 | 59 | this.firstName = firstName; 60 | this.lastName = lastName; 61 | this.role = role; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /simplified/src/main/java/org/springframework/hateoas/examples/EmployeeController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 19 | 20 | import java.net.URI; 21 | import java.net.URISyntaxException; 22 | import java.util.List; 23 | import java.util.stream.Collectors; 24 | import java.util.stream.StreamSupport; 25 | 26 | import org.springframework.hateoas.CollectionModel; 27 | import org.springframework.hateoas.EntityModel; 28 | import org.springframework.hateoas.IanaLinkRelations; 29 | import org.springframework.hateoas.Link; 30 | import org.springframework.http.ResponseEntity; 31 | import org.springframework.web.bind.annotation.GetMapping; 32 | import org.springframework.web.bind.annotation.PathVariable; 33 | import org.springframework.web.bind.annotation.PostMapping; 34 | import org.springframework.web.bind.annotation.PutMapping; 35 | import org.springframework.web.bind.annotation.RequestBody; 36 | import org.springframework.web.bind.annotation.RestController; 37 | 38 | /** 39 | * Spring Web {@link RestController} used to generate a REST API. 40 | * 41 | * @author Greg Turnquist 42 | */ 43 | @RestController 44 | class EmployeeController { 45 | 46 | private final EmployeeRepository repository; 47 | 48 | EmployeeController(EmployeeRepository repository) { 49 | this.repository = repository; 50 | } 51 | 52 | /** 53 | * Look up all employees, and transform them into a REST collection resource. Then return them through Spring Web's 54 | * {@link ResponseEntity} fluent API. 55 | */ 56 | @GetMapping("/employees") 57 | ResponseEntity>> findAll() { 58 | 59 | List> employees = StreamSupport.stream(repository.findAll().spliterator(), false) 60 | .map(employee -> EntityModel.of(employee, // 61 | linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), // 62 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) // 63 | .collect(Collectors.toList()); 64 | 65 | return ResponseEntity.ok( // 66 | CollectionModel.of(employees, // 67 | linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel())); 68 | } 69 | 70 | @PostMapping("/employees") 71 | ResponseEntity newEmployee(@RequestBody Employee employee) { 72 | 73 | try { 74 | Employee savedEmployee = repository.save(employee); 75 | 76 | EntityModel employeeResource = EntityModel.of(savedEmployee, // 77 | linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel()); 78 | 79 | return ResponseEntity // 80 | .created(new URI(employeeResource.getRequiredLink(IanaLinkRelations.SELF).getHref())) // 81 | .body(employeeResource); 82 | } catch (URISyntaxException e) { 83 | return ResponseEntity.badRequest().body("Unable to create " + employee); 84 | } 85 | } 86 | 87 | /** 88 | * Look up a single {@link Employee} and transform it into a REST resource. Then return it through Spring Web's 89 | * {@link ResponseEntity} fluent API. 90 | * 91 | * @param id 92 | */ 93 | @GetMapping("/employees/{id}") 94 | ResponseEntity> findOne(@PathVariable long id) { 95 | 96 | return repository.findById(id) // 97 | .map(employee -> EntityModel.of(employee, // 98 | linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(), // 99 | linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees"))) // 100 | .map(ResponseEntity::ok) // 101 | .orElse(ResponseEntity.notFound().build()); 102 | } 103 | 104 | /** 105 | * Update existing employee then return a Location header. 106 | * 107 | * @param employee 108 | * @param id 109 | * @return 110 | */ 111 | @PutMapping("/employees/{id}") 112 | ResponseEntity updateEmployee(@RequestBody Employee employee, @PathVariable long id) { 113 | 114 | Employee employeeToUpdate = employee; 115 | employeeToUpdate.setId(id); 116 | repository.save(employeeToUpdate); 117 | 118 | Link newlyCreatedLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel(); 119 | 120 | try { 121 | return ResponseEntity.noContent().location(new URI(newlyCreatedLink.getHref())).build(); 122 | } catch (URISyntaxException e) { 123 | return ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate); 124 | } 125 | } 126 | 127 | } 128 | -------------------------------------------------------------------------------- /simplified/src/main/java/org/springframework/hateoas/examples/EmployeeRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import org.springframework.data.repository.CrudRepository; 19 | 20 | /** 21 | * A simple Spring Data {@link CrudRepository} for storing {@link Employee}s. 22 | * 23 | * @author Greg Turnquist 24 | */ 25 | interface EmployeeRepository extends CrudRepository {} 26 | -------------------------------------------------------------------------------- /simplified/src/main/java/org/springframework/hateoas/examples/SpringHateoasSimplifiedApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | import org.springframework.context.annotation.Bean; 22 | import org.springframework.hateoas.server.core.EvoInflectorLinkRelationProvider; 23 | 24 | /** 25 | * @author Greg Turnquist 26 | */ 27 | @SpringBootApplication 28 | public class SpringHateoasSimplifiedApplication { 29 | 30 | public static void main(String... args) { 31 | SpringApplication.run(SpringHateoasSimplifiedApplication.class); 32 | } 33 | 34 | /** 35 | * Format embedded collections by pluralizing the resource's type. 36 | * 37 | * @return 38 | */ 39 | @Bean 40 | EvoInflectorLinkRelationProvider relProvider() { 41 | return new EvoInflectorLinkRelationProvider(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /simplified/src/test/java/org/springframework/hateoas/examples/EmployeeControllerTests.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | import static org.hamcrest.CoreMatchers.*; 19 | import static org.mockito.BDDMockito.*; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 23 | 24 | import java.util.Arrays; 25 | 26 | import org.junit.Test; 27 | import org.junit.runner.RunWith; 28 | import org.springframework.beans.factory.annotation.Autowired; 29 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 30 | import org.springframework.boot.test.mock.mockito.MockBean; 31 | import org.springframework.hateoas.MediaTypes; 32 | import org.springframework.http.HttpHeaders; 33 | import org.springframework.test.context.junit4.SpringRunner; 34 | import org.springframework.test.web.servlet.MockMvc; 35 | 36 | /** 37 | * How to test the hypermedia-based {@link EmployeeController} with everything else mocked out. 38 | * 39 | * @author Greg Turnquist 40 | */ 41 | @RunWith(SpringRunner.class) 42 | @WebMvcTest(EmployeeController.class) 43 | public class EmployeeControllerTests { 44 | 45 | @Autowired private MockMvc mvc; 46 | 47 | @MockBean private EmployeeRepository repository; 48 | 49 | @Test 50 | public void getShouldFetchAHalDocument() throws Exception { 51 | 52 | given(repository.findAll()).willReturn( // 53 | Arrays.asList( // 54 | new Employee(1L, "Frodo", "Baggins", "ring bearer"), // 55 | new Employee(2L, "Bilbo", "Baggins", "burglar"))); 56 | 57 | mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE)) // 58 | .andDo(print()) // 59 | .andExpect(status().isOk()) // 60 | .andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_VALUE)) 61 | .andExpect(jsonPath("$._embedded.employees[0].id", is(1))) 62 | .andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo"))) 63 | .andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins"))) 64 | .andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer"))) 65 | .andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1"))) 66 | .andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees"))) 67 | .andExpect(jsonPath("$._embedded.employees[1].id", is(2))) 68 | .andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo"))) 69 | .andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins"))) 70 | .andExpect(jsonPath("$._embedded.employees[1].role", is("burglar"))) 71 | .andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2"))) 72 | .andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees"))) 73 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/employees"))) // 74 | .andReturn(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-hateoas-examples-spring-data-rest 8 | Spring HATEOAS - Examples - Spring Data REST 9 | jar 10 | 11 | 12 | org.springframework.hateoas.examples 13 | spring-hateoas-examples 14 | 1.0.0.BUILD-SNAPSHOT 15 | 16 | 17 | 18 | 19 | org.springframework.hateoas.examples 20 | commons 21 | 1.0.0.BUILD-SNAPSHOT 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-rest 27 | 28 | 29 | 30 | org.springframework.restdocs 31 | spring-restdocs-webtestclient 32 | test 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | org.springframework.boot 41 | spring-boot-maven-plugin 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/CustomOrderController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import static org.springframework.hateoas.examples.OrderStatus.*; 20 | 21 | import org.springframework.data.rest.webmvc.BasePathAwareController; 22 | import org.springframework.http.ResponseEntity; 23 | import org.springframework.web.bind.annotation.PathVariable; 24 | import org.springframework.web.bind.annotation.PostMapping; 25 | 26 | /** 27 | * @author Greg Turnquist 28 | */ 29 | @BasePathAwareController 30 | public class CustomOrderController { 31 | 32 | private final OrderRepository repository; 33 | 34 | public CustomOrderController(OrderRepository repository) { 35 | this.repository = repository; 36 | } 37 | 38 | @PostMapping("/orders/{id}/pay") 39 | ResponseEntity pay(@PathVariable Long id) { 40 | 41 | Order order = this.repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); 42 | 43 | if (valid(order.getOrderStatus(), OrderStatus.PAID_FOR)) { 44 | 45 | order.setOrderStatus(OrderStatus.PAID_FOR); 46 | return ResponseEntity.ok(repository.save(order)); 47 | } 48 | 49 | return ResponseEntity.badRequest() 50 | .body("Transitioning from " + order.getOrderStatus() + " to " + OrderStatus.PAID_FOR + " is not valid."); 51 | } 52 | 53 | @PostMapping("/orders/{id}/cancel") 54 | ResponseEntity cancel(@PathVariable Long id) { 55 | 56 | Order order = this.repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); 57 | 58 | if (valid(order.getOrderStatus(), OrderStatus.CANCELLED)) { 59 | 60 | order.setOrderStatus(OrderStatus.CANCELLED); 61 | return ResponseEntity.ok(repository.save(order)); 62 | } 63 | 64 | return ResponseEntity.badRequest() 65 | .body("Transitioning from " + order.getOrderStatus() + " to " + OrderStatus.CANCELLED + " is not valid."); 66 | } 67 | 68 | @PostMapping("/orders/{id}/fulfill") 69 | ResponseEntity fulfill(@PathVariable Long id) { 70 | 71 | Order order = this.repository.findById(id).orElseThrow(() -> new OrderNotFoundException(id)); 72 | 73 | if (valid(order.getOrderStatus(), OrderStatus.FULFILLED)) { 74 | 75 | order.setOrderStatus(OrderStatus.FULFILLED); 76 | return ResponseEntity.ok(repository.save(order)); 77 | } 78 | 79 | return ResponseEntity.badRequest() 80 | .body("Transitioning from " + order.getOrderStatus() + " to " + OrderStatus.FULFILLED + " is not valid."); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/DatabaseLoader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import org.springframework.boot.CommandLineRunner; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.stereotype.Component; 22 | 23 | /** 24 | * @author Greg Turnquist 25 | */ 26 | @Component 27 | public class DatabaseLoader { 28 | 29 | @Bean 30 | CommandLineRunner init(OrderRepository repository) { 31 | 32 | return args -> { 33 | repository.save(new Order("grande mocha")); 34 | repository.save(new Order("venti hazelnut machiatto")); 35 | }; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/Order.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import javax.persistence.Entity; 20 | import javax.persistence.GeneratedValue; 21 | import javax.persistence.Id; 22 | import javax.persistence.Table; 23 | import java.util.Objects; 24 | 25 | /** 26 | * @author Greg Turnquist 27 | */ 28 | @Entity 29 | @Table(name = "ORDERS") 30 | class Order { 31 | 32 | @Id @GeneratedValue 33 | private Long id; 34 | 35 | private OrderStatus orderStatus; 36 | 37 | private String description; 38 | 39 | private Order() { 40 | this.id = null; 41 | this.orderStatus = OrderStatus.BEING_CREATED; 42 | this.description = ""; 43 | } 44 | 45 | public Order(String description) { 46 | this(); 47 | this.description = description; 48 | } 49 | 50 | public Long getId() { 51 | return id; 52 | } 53 | 54 | public void setId(Long id) { 55 | this.id = id; 56 | } 57 | 58 | public OrderStatus getOrderStatus() { 59 | return orderStatus; 60 | } 61 | 62 | public void setOrderStatus(OrderStatus orderStatus) { 63 | this.orderStatus = orderStatus; 64 | } 65 | 66 | public String getDescription() { 67 | return description; 68 | } 69 | 70 | public void setDescription(String description) { 71 | this.description = description; 72 | } 73 | 74 | @Override 75 | public boolean equals(Object o) { 76 | if (this == o) { 77 | return true; 78 | } 79 | if (o == null || getClass() != o.getClass()) { 80 | return false; 81 | } 82 | Order order = (Order) o; 83 | return Objects.equals(id, order.id) && 84 | orderStatus == order.orderStatus && 85 | Objects.equals(description, order.description); 86 | } 87 | 88 | @Override 89 | public int hashCode() { 90 | return Objects.hash(id, orderStatus, description); 91 | } 92 | 93 | @Override 94 | public String toString() { 95 | return "Order{" + 96 | "id=" + id + 97 | ", orderStatus=" + orderStatus + 98 | ", description='" + description + '\'' + 99 | '}'; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/OrderNotFoundException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.hateoas.examples; 17 | 18 | /** 19 | * @author Greg Turnquist 20 | */ 21 | class OrderNotFoundException extends RuntimeException { 22 | 23 | public OrderNotFoundException(Long id) { 24 | super("Order " + id + " not found!"); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/OrderProcessor.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import static org.springframework.hateoas.examples.OrderStatus.*; 20 | import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*; 21 | 22 | import java.net.URI; 23 | import java.net.URISyntaxException; 24 | 25 | import org.springframework.data.rest.core.config.RepositoryRestConfiguration; 26 | import org.springframework.hateoas.EntityModel; 27 | import org.springframework.hateoas.IanaLinkRelations; 28 | import org.springframework.hateoas.Link; 29 | import org.springframework.hateoas.LinkRelation; 30 | import org.springframework.hateoas.server.RepresentationModelProcessor; 31 | import org.springframework.stereotype.Component; 32 | 33 | /** 34 | * A {@link RepresentationModelProcessor} that takes an {@link Order} that has been wrapped by Spring Data REST into an 35 | * {@link EntityModel} and applies custom Spring HATEAOS-based {@link Link}s based on the state. 36 | * 37 | * @author Greg Turnquist 38 | */ 39 | @Component 40 | public class OrderProcessor implements RepresentationModelProcessor> { 41 | 42 | private final RepositoryRestConfiguration configuration; 43 | 44 | public OrderProcessor(RepositoryRestConfiguration configuration) { 45 | this.configuration = configuration; 46 | } 47 | 48 | @Override 49 | public EntityModel process(EntityModel model) { 50 | 51 | CustomOrderController controller = methodOn(CustomOrderController.class); 52 | String basePath = configuration.getBasePath().toString(); 53 | 54 | // If PAID_FOR is valid, add a link to the `pay()` method 55 | if (valid(model.getContent().getOrderStatus(), OrderStatus.PAID_FOR)) { 56 | model.add(applyBasePath( // 57 | linkTo(controller.pay(model.getContent().getId())) // 58 | .withRel(IanaLinkRelations.PAYMENT), // 59 | basePath)); 60 | } 61 | 62 | // If CANCELLED is valid, add a link to the `cancel()` method 63 | if (valid(model.getContent().getOrderStatus(), OrderStatus.CANCELLED)) { 64 | model.add(applyBasePath( // 65 | linkTo(controller.cancel(model.getContent().getId())) // 66 | .withRel(LinkRelation.of("cancel")), // 67 | basePath)); 68 | } 69 | 70 | // If FULFILLED is valid, add a link to the `fulfill()` method 71 | if (valid(model.getContent().getOrderStatus(), OrderStatus.FULFILLED)) { 72 | model.add(applyBasePath( // 73 | linkTo(controller.fulfill(model.getContent().getId())) // 74 | .withRel(LinkRelation.of("fulfill")), // 75 | basePath)); 76 | } 77 | 78 | return model; 79 | } 80 | 81 | /** 82 | * Adjust the {@link Link} such that it starts at {@literal basePath}. 83 | * 84 | * @param link - link presumably supplied via Spring HATEOAS 85 | * @param basePath - base path provided by Spring Data REST 86 | * @return new {@link Link} with these two values melded together 87 | */ 88 | private static Link applyBasePath(Link link, String basePath) { 89 | 90 | URI uri = link.toUri(); 91 | 92 | URI newUri = null; 93 | try { 94 | newUri = new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), // 95 | uri.getPort(), basePath + uri.getPath(), uri.getQuery(), uri.getFragment()); 96 | } catch (URISyntaxException e) { 97 | e.printStackTrace(); 98 | } 99 | 100 | return new Link(newUri.toString(), link.getRel()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/OrderRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import org.springframework.data.repository.CrudRepository; 20 | 21 | /** 22 | * @author Greg Turnquist 23 | */ 24 | public interface OrderRepository extends CrudRepository { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/OrderStatus.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | /** 20 | * @author Greg Turnquist 21 | */ 22 | public enum OrderStatus { 23 | 24 | BEING_CREATED, PAID_FOR, FULFILLED, CANCELLED; 25 | 26 | /** 27 | * Verify the transition between {@link OrderStatus} is valid. NOTE: This is where any/all rules for state transitions 28 | * should be kept and enforced. 29 | */ 30 | static boolean valid(OrderStatus currentStatus, OrderStatus newStatus) { 31 | 32 | if (currentStatus == BEING_CREATED) { 33 | return newStatus == PAID_FOR || newStatus == CANCELLED; 34 | } else if (currentStatus == PAID_FOR) { 35 | return newStatus == FULFILLED; 36 | } else if (currentStatus == FULFILLED) { 37 | return false; 38 | } else if (currentStatus == CANCELLED) { 39 | return false; 40 | } else { 41 | throw new RuntimeException("Unrecognized situation."); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/java/org/springframework/hateoas/examples/SpringHateoasSpringDataRestApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | 22 | /** 23 | * @author Greg Turnquist 24 | */ 25 | @SpringBootApplication 26 | public class SpringHateoasSpringDataRestApplication { 27 | 28 | public static void main(String[] args) { 29 | SpringApplication.run(SpringHateoasSpringDataRestApplication.class); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | data: 3 | rest: 4 | base-path: /api -------------------------------------------------------------------------------- /spring-hateoas-and-spring-data-rest/src/test/java/org/springframework/hateoas/examples/OrderIntegrationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.hateoas.examples; 18 | 19 | import static org.hamcrest.Matchers.*; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 23 | 24 | import org.junit.jupiter.api.Test; 25 | import org.springframework.beans.factory.annotation.Autowired; 26 | import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; 27 | import org.springframework.boot.test.context.SpringBootTest; 28 | import org.springframework.hateoas.MediaTypes; 29 | import org.springframework.http.MediaType; 30 | import org.springframework.test.web.servlet.MockMvc; 31 | 32 | /** 33 | * @author Greg Turnquist 34 | */ 35 | @SpringBootTest() 36 | @AutoConfigureMockMvc 37 | public class OrderIntegrationTest { 38 | 39 | @Autowired MockMvc mvc; 40 | 41 | @Test 42 | void basics() throws Exception { 43 | 44 | // Core operations provided by Spring Data REST 45 | 46 | this.mvc.perform(get("/api")) // 47 | .andDo(print()) // 48 | .andExpect(status().isOk()) // 49 | .andExpect(content().contentType(MediaTypes.HAL_JSON)) // 50 | .andExpect(jsonPath("$._links.orders.href", is("http://localhost/api/orders"))) 51 | .andExpect(jsonPath("$._links.profile.href", is("http://localhost/api/profile"))); 52 | 53 | this.mvc.perform(get("/api/orders")).andDo(print()) // 54 | .andExpect(status().isOk()) // 55 | .andExpect(content().contentType(MediaTypes.HAL_JSON)) // 56 | .andExpect(jsonPath("$._embedded.orders[0].orderStatus", is("BEING_CREATED"))) 57 | .andExpect(jsonPath("$._embedded.orders[0].description", is("grande mocha"))) 58 | .andExpect(jsonPath("$._embedded.orders[0]._links.self.href", is("http://localhost/api/orders/1"))) 59 | .andExpect(jsonPath("$._embedded.orders[0]._links.order.href", is("http://localhost/api/orders/1"))) 60 | .andExpect(jsonPath("$._embedded.orders[0]._links.payment.href", is("http://localhost/api/orders/1/pay"))) 61 | .andExpect(jsonPath("$._embedded.orders[0]._links.cancel.href", is("http://localhost/api/orders/1/cancel"))) 62 | .andExpect(jsonPath("$._embedded.orders[1].orderStatus", is("BEING_CREATED"))) 63 | .andExpect(jsonPath("$._embedded.orders[1].description", is("venti hazelnut machiatto"))) 64 | .andExpect(jsonPath("$._embedded.orders[1]._links.self.href", is("http://localhost/api/orders/2"))) 65 | .andExpect(jsonPath("$._embedded.orders[1]._links.order.href", is("http://localhost/api/orders/2"))) 66 | .andExpect(jsonPath("$._embedded.orders[1]._links.payment.href", is("http://localhost/api/orders/2/pay"))) 67 | .andExpect(jsonPath("$._embedded.orders[1]._links.cancel.href", is("http://localhost/api/orders/2/cancel"))) 68 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/api/orders"))) 69 | .andExpect(jsonPath("$._links.profile.href", is("http://localhost/api/profile/orders"))); 70 | 71 | // Fulfilling an unpaid-for order should fail. 72 | 73 | this.mvc.perform(post("/api/orders/1/fulfill")) // 74 | .andDo(print()) // 75 | .andExpect(status().is4xxClientError()) // 76 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 77 | .andExpect(content().string("\"Transitioning from BEING_CREATED to FULFILLED is not valid.\"")); 78 | 79 | // Pay for the order. 80 | 81 | this.mvc.perform(post("/api/orders/1/pay")) // 82 | .andDo(print()) // 83 | .andExpect(status().isOk()) // 84 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 85 | .andExpect(jsonPath("$.id", is(1))) // 86 | .andExpect(jsonPath("$.orderStatus", is("PAID_FOR"))); 87 | 88 | // Paying for an already paid-for order should fail. 89 | 90 | this.mvc.perform(post("/api/orders/1/pay")) // 91 | .andDo(print()) // 92 | .andExpect(status().is4xxClientError()) // 93 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 94 | .andExpect(content().string("\"Transitioning from PAID_FOR to PAID_FOR is not valid.\"")); 95 | 96 | // Cancelling a paid-for order should fail. 97 | 98 | this.mvc.perform(post("/api/orders/1/cancel")) // 99 | .andDo(print()) // 100 | .andExpect(status().is4xxClientError()) // 101 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 102 | .andExpect(content().string("\"Transitioning from PAID_FOR to CANCELLED is not valid.\"")); 103 | 104 | // Verify a paid-for order now shows links to fulfill. 105 | 106 | this.mvc.perform(get("/api/orders/1")) // 107 | .andDo(print()) // 108 | .andExpect(status().isOk()) // 109 | .andExpect(content().contentType(MediaTypes.HAL_JSON)) // 110 | .andExpect(jsonPath("$.orderStatus", is("PAID_FOR"))) // 111 | .andExpect(jsonPath("$.description", is("grande mocha"))) // 112 | .andExpect(jsonPath("$._links.self.href", is("http://localhost/api/orders/1"))) 113 | .andExpect(jsonPath("$._links.order.href", is("http://localhost/api/orders/1"))) 114 | .andExpect(jsonPath("$._links.fulfill.href", is("http://localhost/api/orders/1/fulfill"))); 115 | 116 | // Fulfill the order. 117 | 118 | this.mvc.perform(post("/api/orders/1/fulfill")) // 119 | .andDo(print()) // 120 | .andExpect(status().isOk()) // 121 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 122 | .andExpect(jsonPath("$.orderStatus", is("FULFILLED"))) // 123 | .andExpect(jsonPath("$.description", is("grande mocha"))); 124 | 125 | // Cancelling a fulfilled order should fail. 126 | 127 | this.mvc.perform(post("/api/orders/1/cancel")) // 128 | .andDo(print()) // 129 | .andExpect(status().is4xxClientError()) // 130 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 131 | .andExpect(content().string("\"Transitioning from FULFILLED to CANCELLED is not valid.\"")); 132 | 133 | // Cancel an order. 134 | 135 | this.mvc.perform(post("/api/orders/2/cancel")) // 136 | .andDo(print()) // 137 | .andExpect(status().isOk()) // 138 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) // 139 | .andExpect(jsonPath("$.orderStatus", is("CANCELLED"))) // 140 | .andExpect(jsonPath("$.description", is("venti hazelnut machiatto"))); 141 | } 142 | 143 | } 144 | --------------------------------------------------------------------------------