#992 - Remove URI restriction in HAL-FORMS implementation.

Prior to this commit the HAL-FORMS implementation verified the URIs of the affordances to match the self link of the representation model. This prevents the implementation of the HAL-FORMS metadata lookup protocol as defined in the HAL-FORMS specification [0].

We removed the verification to allow alternate URIs to be used for the afforances.

[0] https://rwcbook.github.io/hal-forms/#_suggested_process_flow_for_hal_forms_documents
This commit is contained in:
Oliver Drotbohm
2019-06-27 14:33:12 +02:00
parent 628148a169
commit 440e1852cc
2 changed files with 0 additions and 229 deletions

View File

@@ -280,7 +280,6 @@ class HalFormsSerializers {
.map(it -> it.getAffordanceModel(MediaTypes.HAL_FORMS_JSON)) //
.map(HalFormsAffordanceModel.class::cast) //
.filter(it -> !it.hasHttpMethod(HttpMethod.GET)) //
.peek(it -> validate(resource, it)) //
.forEach(it -> {
Class<?> type = it.getInputType().resolve(Object.class);
@@ -388,21 +387,4 @@ class HalFormsSerializers {
return new String[] { qualifiedCode, localCode, globalCode };
}
}
/**
* Verify that the resource's self link and the affordance's URI have the same relative path.
*
* @param resource
* @param model
*/
private static void validate(RepresentationModel<?> resource, HalFormsAffordanceModel model) {
String affordanceUri = model.getURI();
String selfLinkUri = resource.getRequiredLink(IanaLinkRelations.SELF.value()).expand().getHref();
if (!affordanceUri.equals(selfLinkUri)) {
throw new IllegalStateException("Affordance's URI " + affordanceUri + " doesn't match self link " + selfLinkUri
+ " as expected in HAL-FORMS");
}
}
}

View File

@@ -1,211 +0,0 @@
/*
* 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
*
* 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 org.springframework.hateoas.mediatype.hal.forms;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.CollectionModel;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.IanaLinkRelations;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.hateoas.support.Employee;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
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;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
/**
* Test that when an {@link org.springframework.hateoas.Affordance} is included that does NOT match the self link, an
* exception is thrown.
*
* @author Greg Turnquist
*/
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration
class HalFormsValidationIntegrationTest {
@Autowired WebApplicationContext context;
MockMvc mockMvc;
@BeforeEach
void setUp() {
this.mockMvc = webAppContextSetup(this.context).build();
}
@Test
void singleEmployee() throws Exception {
Exception exception = this.mockMvc.perform(get("/employees/0").accept(MediaTypes.HAL_FORMS_JSON))
.andExpect(status().is5xxServerError()) //
.andReturn() //
.getResolvedException();
assertThat(exception).isNotNull();
assertThat(exception.getMessage()).contains("Affordance's URI http://localhost/employees");
assertThat(exception.getMessage()).contains("doesn't match self link http://localhost/employees/0");
}
@Test
void collectionOfEmployees() throws Exception {
Exception exception = this.mockMvc.perform(get("/employees").accept(MediaTypes.HAL_FORMS_JSON)) //
.andExpect(status().is5xxServerError()) //
.andReturn().getResolvedException();
assertThat(exception).isNotNull();
assertThat(exception.getMessage()).contains("Affordance's URI http://localhost/employees/0");
assertThat(exception.getMessage()).contains("doesn't match self link http://localhost/employees");
}
/**
* This controller violates HAL-FORMS spec requirements. We use it to verify the serializers can catch it.
*/
@RestController
static class BadController {
private final static Map<Integer, Employee> EMPLOYEES = new TreeMap<>();
static {
EMPLOYEES.put(0, new Employee("Frodo Baggins", "ring bearer"));
EMPLOYEES.put(1, new Employee("Bilbo Baggins", "burglar"));
}
@GetMapping("/employees")
public CollectionModel<EntityModel<Employee>> all() {
// Create a list of Resource<Employee>'s to return
List<EntityModel<Employee>> employees = new ArrayList<>();
// Fetch each Resource<Employee> using the controller's findOne method.
for (int i = 0; i < EMPLOYEES.size(); i++) {
employees.add(findOne(i));
}
// Generate an "Affordance" based on this method (the "self" link)
Link selfLink = linkTo(methodOn(BadController.class).all()).withSelfRel()
.andAffordance(afford(methodOn(BadController.class).updateEmployee(null, 0)));
// Return the collection of employee resources along with the composite affordance
return new CollectionModel<>(employees, selfLink);
}
@GetMapping("/employees/{id}")
public EntityModel<Employee> findOne(@PathVariable Integer id) {
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(BadController.class).findOne(id)).withSelfRel();
// Define final link as means to find entire collection.
Link employeesLink = linkTo(methodOn(BadController.class).all()).withRel("employees")
.andAffordance(afford(methodOn(BadController.class).newEmployee(null)));
// Return the affordance + a link back to the entire collection resource.
return new EntityModel<>(EMPLOYEES.get(id), findOneLink.andAffordances(employeesLink.getAffordances()),
employeesLink);
}
@PostMapping("/employees")
public ResponseEntity<?> newEmployee(@RequestBody Employee employee) {
int newEmployeeId = EMPLOYEES.size();
EMPLOYEES.put(newEmployeeId, employee);
return ResponseEntity.noContent() //
.location(findOne(newEmployeeId) //
.getRequiredLink(IanaLinkRelations.SELF) //
.toUri()) //
.build();
}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable Integer id) {
EMPLOYEES.put(id, employee);
return ResponseEntity //
.noContent() //
.location(findOne(id) //
.getRequiredLink(IanaLinkRelations.SELF) //
.toUri()) //
.build();
}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable Integer id) {
Employee newEmployee = EMPLOYEES.get(id);
if (employee.getName() != null) {
newEmployee = newEmployee.withName(employee.getName());
}
if (employee.getRole() != null) {
newEmployee = newEmployee.withRole(employee.getRole());
}
EMPLOYEES.put(id, newEmployee);
return ResponseEntity //
.noContent() //
.location(findOne(id) //
.getRequiredLink(IanaLinkRelations.SELF) //
.toUri()) //
.build();
}
}
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type = { HypermediaType.HAL_FORMS })
static class TestConfig {
@Bean
BadController employeeController() {
return new BadController();
}
}
}