Add 'simplified' version

This commit is contained in:
Greg Turnquist
2018-02-12 17:05:31 -06:00
parent d8c7f97a37
commit 2723283640
10 changed files with 654 additions and 1 deletions

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2017 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
*
* http://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 org.springframework.hateoas.examples;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
/**
* Pre-load some data using a Spring Boot {@link CommandLineRunner}.
*
* @author Greg Turnquist
*/
@Component
class DatabaseLoader {
/**
* Use Spring to inject a {@link EmployeeRepository} that can then load data. Since this will run
* only after the app is operational, the database will be up.
*
* @param repository
*/
@Bean
CommandLineRunner init(EmployeeRepository repository) {
return args -> {
repository.save(new Employee("Frodo", "Baggins", "ring bearer"));
repository.save(new Employee("Bilbo", "Baggins", "burglar"));
};
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2017 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
*
* http://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 org.springframework.hateoas.examples;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
/**
* Domain object representing a company employee. Project Lombok keeps actual code at a minimum.
*
* {@code @Data} - Generates getters, setters, toString, hash, and equals functions
* {@code @Entity} - JPA annotation to flag this class for DB persistence
* {@code @NoArgsConstructor} - Create a constructor with no args to support JPA
* {@code @AllArgsConstructor} - Create a constructor with all args to support testing
*
* {@code @JsonIgnoreProperties(ignoreUnknow=true)}
* When converting JSON to Java, ignore any unrecognized attributes. This is critical for REST because it
* encourages adding new fields in later versions that won't break. It also allows things like _links to be
* ignore as well, meaning HAL documents can be fetched and later posted to the server without adjustment.
*
*
* @author Greg Turnquist
*/
@Data
@Entity
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor
class Employee {
@Id @GeneratedValue
private Long id;
private String firstName;
private String lastName;
private String role;
/**
* Useful constructor when id is not yet known.
*
* @param firstName
* @param lastName
* @param role
*/
Employee(String firstName, String lastName, String role) {
this.firstName = firstName;
this.lastName = lastName;
this.role = role;
}
}

View File

@@ -0,0 +1,135 @@
/*
* Copyright 2017 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
*
* http://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 org.springframework.hateoas.examples;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
/**
* Spring Web {@link RestController} used to generate a REST API.
*
* @author Greg Turnquist
*/
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
/**
* Look up all employees, and transform them into a REST collection resource.
* Then return them through Spring Web's {@link ResponseEntity} fluent API.
*
* NOTE: cURL will fetch things as HAL JSON directly, but browsers issue a different
* default accept header, which allows XML to get requested first, so "produces"
* forces it to HAL JSON for all clients.
*/
@GetMapping(value = "/employees", produces = MediaTypes.HAL_JSON_VALUE)
ResponseEntity<Resources<Resource<Employee>>> findAll() {
List<Resource<Employee>> employees = StreamSupport.stream(repository.findAll().spliterator(), false)
.map(employee -> new Resource<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.collect(Collectors.toList());
return ResponseEntity.ok(
new Resources<>(employees,
linkTo(methodOn(EmployeeController.class).findAll()).withSelfRel()));
}
@PostMapping("/employees")
ResponseEntity<?> newEmployee(@RequestBody Employee employee) {
try {
Employee savedEmployee = repository.save(employee);
Resource<Employee> employeeResource = new Resource<>(savedEmployee,
linkTo(methodOn(EmployeeController.class).findOne(savedEmployee.getId())).withSelfRel());
return ResponseEntity
.created(new URI(employeeResource.getRequiredLink(Link.REL_SELF).getHref()))
.body(employeeResource);
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body("Unable to create " + employee);
}
}
/**
* Look up a single {@link Employee} and transform it into a REST resource. Then return it through
* Spring Web's {@link ResponseEntity} fluent API.
*
* See {@link #findAll()} to explain {@link GetMapping}'s "produces" argument.
*
* @param id
*/
@GetMapping(value = "/employees/{id}", produces = MediaTypes.HAL_JSON_VALUE)
ResponseEntity<Resource<Employee>> findOne(@PathVariable long id) {
return repository.findById(id)
.map(employee -> new Resource<>(employee,
linkTo(methodOn(EmployeeController.class).findOne(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).findAll()).withRel("employees")))
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
/**
* Update existing employee then return a Location header.
*
* @param employee
* @param id
* @return
*/
@PutMapping("/employees/{id}")
ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable long id) {
Employee employeeToUpdate = employee;
employeeToUpdate.setId(id);
repository.save(employeeToUpdate);
Link newlyCreatedLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
try {
return ResponseEntity.noContent()
.location(new URI(newlyCreatedLink.getHref()))
.build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body("Unable to update " + employeeToUpdate);
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* Copyright 2017 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
*
* http://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 org.springframework.hateoas.examples;
import org.springframework.data.repository.CrudRepository;
/**
* A simple Spring Data {@link CrudRepository} for storing {@link Employee}s.
*
* @author Greg Turnquist
*/
interface EmployeeRepository extends CrudRepository<Employee, Long> {
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2017 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
*
* http://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 org.springframework.hateoas.examples;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.hateoas.core.EvoInflectorRelProvider;
/**
* @author Greg Turnquist
*/
@SpringBootApplication
public class SpringHateoasSimplifiedApplication {
public static void main(String... args) {
SpringApplication.run(SpringHateoasSimplifiedApplication.class);
}
/**
* Format embedded collections by pluralizing the resource's type.
*
* @return
*/
@Bean
EvoInflectorRelProvider relProvider() {
return new EvoInflectorRelProvider();
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2017 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
*
* http://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 org.springframework.hateoas.examples;
import static org.hamcrest.CoreMatchers.*;
import static org.mockito.BDDMockito.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import java.util.Arrays;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.HttpHeaders;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
/**
* How to test the hypermedia-based {@link EmployeeController} with everything else mocked out.
*
* @author Greg Turnquist
*/
@RunWith(SpringRunner.class)
@WebMvcTest(EmployeeController.class)
public class EmployeeControllerTests {
@Autowired
private MockMvc mvc;
@MockBean
private EmployeeRepository repository;
@Test
public void getShouldFetchAHalDocument() throws Exception {
given(repository.findAll()).willReturn(
Arrays.asList(
new Employee(1L,"Frodo", "Baggins", "ring bearer"),
new Employee(2L,"Bilbo", "Baggins", "burglar")));
mvc.perform(get("/employees").accept(MediaTypes.HAL_JSON_VALUE))
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE))
.andExpect(jsonPath("$._embedded.employees[0].id", is(1)))
.andExpect(jsonPath("$._embedded.employees[0].firstName", is("Frodo")))
.andExpect(jsonPath("$._embedded.employees[0].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[0].role", is("ring bearer")))
.andExpect(jsonPath("$._embedded.employees[0]._links.self.href", is("http://localhost/employees/1")))
.andExpect(jsonPath("$._embedded.employees[0]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._embedded.employees[1].id", is(2)))
.andExpect(jsonPath("$._embedded.employees[1].firstName", is("Bilbo")))
.andExpect(jsonPath("$._embedded.employees[1].lastName", is("Baggins")))
.andExpect(jsonPath("$._embedded.employees[1].role", is("burglar")))
.andExpect(jsonPath("$._embedded.employees[1]._links.self.href", is("http://localhost/employees/2")))
.andExpect(jsonPath("$._embedded.employees[1]._links.employees.href", is("http://localhost/employees")))
.andExpect(jsonPath("$._links.self.href", is("http://localhost/employees")))
.andReturn();
}
}