diff --git a/ci/smoke-tests.yml b/ci/smoke-tests.yml index 06243a50..98f074e1 100644 --- a/ci/smoke-tests.yml +++ b/ci/smoke-tests.yml @@ -12,6 +12,7 @@ smoke_tests: - configuration-properties - data-jdbc - data-jpa + - hateoas - jdbc - logging-log4j2 - security-webflux diff --git a/hateoas/README.adoc b/hateoas/README.adoc new file mode 100644 index 00000000..5a4b3ce1 --- /dev/null +++ b/hateoas/README.adoc @@ -0,0 +1 @@ +Tests if Spring HATEOAS is working. diff --git a/hateoas/build.gradle b/hateoas/build.gradle new file mode 100644 index 00000000..253d7c72 --- /dev/null +++ b/hateoas/build.gradle @@ -0,0 +1,19 @@ +plugins { + id 'java' + id 'org.springframework.boot' + id 'org.springframework.aot.smoke-test' + id 'org.graalvm.buildtools.native' +} + +dependencies { + implementation(platform(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)) + implementation("org.springframework.boot:spring-boot-starter-hateoas") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + + aotTestImplementation(project(":aot-smoke-test-support")) +} + +aotSmokeTest { + webApplication = true +} diff --git a/hateoas/src/aotTest/java/com/example/hateoas/HateoasApplicationAotTests.java b/hateoas/src/aotTest/java/com/example/hateoas/HateoasApplicationAotTests.java new file mode 100644 index 00000000..70825d0d --- /dev/null +++ b/hateoas/src/aotTest/java/com/example/hateoas/HateoasApplicationAotTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.hateoas; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.smoketest.support.junit.AotSmokeTest; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +@AotSmokeTest +class HateoasApplicationAotTests { + + @Test + void employeeHasLinks(WebTestClient client) { + client.get().uri("/employee/1").exchange().expectStatus().isOk().expectBody().jsonPath("$.id").isEqualTo("1") + .jsonPath("$._links.self.href").value(v -> { + assertThat(v).isInstanceOf(String.class); + assertThat((String) v).endsWith("/employee/1"); + }).jsonPath("$._links.manager.href").value(v -> { + assertThat(v).isInstanceOf(String.class); + assertThat((String) v).endsWith("/manager/1"); + }); + } + + @Test + void managerHasLinks(WebTestClient client) { + client.get().uri("/manager/1").exchange().expectStatus().isOk().expectBody().jsonPath("$.id").isEqualTo("1") + .jsonPath("$._links.self.href").value(v -> { + assertThat(v).isInstanceOf(String.class); + assertThat((String) v).endsWith("/manager/1"); + }).jsonPath("$._links.reports.href").value(v -> { + assertThat(v).isInstanceOf(String.class); + assertThat((String) v).endsWith("/manager/1/reports"); + }); + } + + @Test + void reportsIsCollection(WebTestClient client) { + client.get().uri("/manager/1/reports").exchange().expectStatus().isOk().expectBody() + .jsonPath("$._links.self.href").value(v -> { + assertThat(v).isInstanceOf(String.class); + assertThat((String) v).endsWith("/manager/1/reports"); + }).jsonPath("$._embedded").isMap().jsonPath("$._embedded.employees").isArray() + .jsonPath("$._embedded.employees[0].id").isEqualTo("1").jsonPath("$._embedded.employees[1].id") + .isEqualTo("2").jsonPath("$._embedded.employees[2].id").isEqualTo("3"); + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/EmployeeController.java b/hateoas/src/main/java/com/example/hateoas/EmployeeController.java new file mode 100644 index 00000000..c16b1e2d --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/EmployeeController.java @@ -0,0 +1,31 @@ +package com.example.hateoas; + +import com.example.hateoas.assembler.EmployeeModelAssembler; +import com.example.hateoas.model.Employee; +import com.example.hateoas.model.EmployeeModel; + +import org.springframework.hateoas.server.ExposesResourceFor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/employee", produces = MediaType.APPLICATION_JSON_VALUE) +@ExposesResourceFor(Employee.class) +public class EmployeeController { + + private final EmployeeModelAssembler employeeModelAssembler; + + public EmployeeController(EmployeeModelAssembler employeeModelAssembler) { + this.employeeModelAssembler = employeeModelAssembler; + } + + @GetMapping("{id}") + public EmployeeModel getById(@PathVariable String id) { + Employee employee = new Employee(id, "first-name", "last-name", id); + return employeeModelAssembler.toModel(employee); + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/HateoasApplication.java b/hateoas/src/main/java/com/example/hateoas/HateoasApplication.java new file mode 100644 index 00000000..a6b000f8 --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/HateoasApplication.java @@ -0,0 +1,16 @@ +package com.example.hateoas; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.hateoas.config.EnableHypermediaSupport; +import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType; + +@SpringBootApplication +@EnableHypermediaSupport(type = HypermediaType.HAL) +public class HateoasApplication { + + public static void main(String[] args) { + SpringApplication.run(HateoasApplication.class, args); + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/ManagerController.java b/hateoas/src/main/java/com/example/hateoas/ManagerController.java new file mode 100644 index 00000000..63f4ea82 --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/ManagerController.java @@ -0,0 +1,52 @@ +package com.example.hateoas; + +import java.util.List; +import java.util.Set; + +import com.example.hateoas.assembler.EmployeeModelAssembler; +import com.example.hateoas.assembler.ManagerModelAssembler; +import com.example.hateoas.model.Employee; +import com.example.hateoas.model.EmployeeModel; +import com.example.hateoas.model.Manager; +import com.example.hateoas.model.ManagerModel; + +import org.springframework.hateoas.CollectionModel; +import org.springframework.hateoas.server.ExposesResourceFor; +import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping(path = "/manager", produces = MediaType.APPLICATION_JSON_VALUE) +@ExposesResourceFor(Manager.class) +public class ManagerController { + + private final EmployeeModelAssembler employeeModelAssembler; + + private final ManagerModelAssembler managerModelAssembler; + + ManagerController(EmployeeModelAssembler employeeModelAssembler, ManagerModelAssembler managerModelAssembler) { + this.employeeModelAssembler = employeeModelAssembler; + this.managerModelAssembler = managerModelAssembler; + } + + @GetMapping("{id}") + public ManagerModel getById(@PathVariable String id) { + Manager manager = new Manager(id, "first-name", "last-name", Set.of("1", "2", "3")); + return this.managerModelAssembler.toModel(manager); + } + + @GetMapping("{id}/reports") + public CollectionModel reports(@PathVariable String id) { + List employees = List.of(new Employee("1", "first-name-1", "last-name-1", "1"), + new Employee("2", "first-name-2", "last-name-2", "1"), + new Employee("3", "first-name-3", "last-name-3", "1")); + return employeeModelAssembler.toCollectionModel(employees).withFallbackType(EmployeeModel.class) + .add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(ManagerController.class).reports(id)) + .withSelfRel()); + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/assembler/EmployeeModelAssembler.java b/hateoas/src/main/java/com/example/hateoas/assembler/EmployeeModelAssembler.java new file mode 100644 index 00000000..9b732042 --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/assembler/EmployeeModelAssembler.java @@ -0,0 +1,34 @@ +package com.example.hateoas.assembler; + +import com.example.hateoas.EmployeeController; +import com.example.hateoas.model.Employee; +import com.example.hateoas.model.EmployeeModel; +import com.example.hateoas.model.Manager; + +import org.springframework.hateoas.server.EntityLinks; +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.stereotype.Component; + +@Component +public class EmployeeModelAssembler extends RepresentationModelAssemblerSupport { + + private final EntityLinks entityLinks; + + EmployeeModelAssembler(EntityLinks entityLinks) { + super(EmployeeController.class, EmployeeModel.class); + this.entityLinks = entityLinks; + } + + @Override + protected EmployeeModel instantiateModel(Employee entity) { + return new EmployeeModel(entity); + } + + @Override + public EmployeeModel toModel(Employee entity) { + EmployeeModel model = createModelWithId(entity.getId(), entity); + model.add(this.entityLinks.linkToItemResource(Manager.class, entity.getManagerId()).withRel("manager")); + return model; + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/assembler/ManagerModelAssembler.java b/hateoas/src/main/java/com/example/hateoas/assembler/ManagerModelAssembler.java new file mode 100644 index 00000000..87b9c9dd --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/assembler/ManagerModelAssembler.java @@ -0,0 +1,31 @@ +package com.example.hateoas.assembler; + +import com.example.hateoas.ManagerController; +import com.example.hateoas.model.Manager; +import com.example.hateoas.model.ManagerModel; + +import org.springframework.hateoas.server.mvc.RepresentationModelAssemblerSupport; +import org.springframework.hateoas.server.mvc.WebMvcLinkBuilder; +import org.springframework.stereotype.Component; + +@Component +public class ManagerModelAssembler extends RepresentationModelAssemblerSupport { + + public ManagerModelAssembler() { + super(ManagerController.class, ManagerModel.class); + } + + @Override + public ManagerModel toModel(Manager entity) { + ManagerModel model = createModelWithId(entity.getId(), entity); + model.add(WebMvcLinkBuilder.linkTo(WebMvcLinkBuilder.methodOn(ManagerController.class).reports(entity.getId())) + .withRel("reports")); + return model; + } + + @Override + protected ManagerModel instantiateModel(Manager entity) { + return new ManagerModel(entity); + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/model/Employee.java b/hateoas/src/main/java/com/example/hateoas/model/Employee.java new file mode 100644 index 00000000..b173ed39 --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/model/Employee.java @@ -0,0 +1,39 @@ +package com.example.hateoas.model; + +import org.springframework.hateoas.server.core.Relation; + +@Relation(itemRelation = "employee", collectionRelation = "employees") +public class Employee { + + private final String id; + + private final String firstName; + + private final String lastName; + + private final String managerId; + + public Employee(String id, String firstName, String lastName, String managerId) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.managerId = managerId; + } + + public String getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getManagerId() { + return managerId; + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/model/EmployeeModel.java b/hateoas/src/main/java/com/example/hateoas/model/EmployeeModel.java new file mode 100644 index 00000000..baa63622 --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/model/EmployeeModel.java @@ -0,0 +1,11 @@ +package com.example.hateoas.model; + +import org.springframework.hateoas.EntityModel; + +public class EmployeeModel extends EntityModel { + + public EmployeeModel(Employee employee) { + super(employee); + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/model/Manager.java b/hateoas/src/main/java/com/example/hateoas/model/Manager.java new file mode 100644 index 00000000..a9a8fd9d --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/model/Manager.java @@ -0,0 +1,41 @@ +package com.example.hateoas.model; + +import java.util.Set; + +import org.springframework.hateoas.server.core.Relation; + +@Relation(itemRelation = "manager", collectionRelation = "managers") +public class Manager { + + private final String id; + + private final String firstName; + + private final String lastName; + + private final Set reports; + + public Manager(String id, String firstName, String lastName, Set reports) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.reports = reports; + } + + public String getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public Set getReports() { + return reports; + } + +} diff --git a/hateoas/src/main/java/com/example/hateoas/model/ManagerModel.java b/hateoas/src/main/java/com/example/hateoas/model/ManagerModel.java new file mode 100644 index 00000000..23e9916a --- /dev/null +++ b/hateoas/src/main/java/com/example/hateoas/model/ManagerModel.java @@ -0,0 +1,11 @@ +package com.example.hateoas.model; + +import org.springframework.hateoas.EntityModel; + +public class ManagerModel extends EntityModel { + + public ManagerModel(Manager manager) { + super(manager); + } + +} diff --git a/hateoas/src/test/java/com/example/hateoas/HateoasApplicationTests.java b/hateoas/src/test/java/com/example/hateoas/HateoasApplicationTests.java new file mode 100644 index 00000000..2df16975 --- /dev/null +++ b/hateoas/src/test/java/com/example/hateoas/HateoasApplicationTests.java @@ -0,0 +1,14 @@ +package com.example.hateoas; + +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class HateoasApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/settings.gradle b/settings.gradle index 2f1ed65c..5f74f6c5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -40,6 +40,7 @@ include "conditional" include "configuration-properties" include "data-jdbc" include "data-jpa" +include "hateoas" include "jdbc" include "logging-log4j2" include "scheduled"