#695 - Remove Jaxb annotations.

By removing all Jaxb annotations, requesting data from a hypermedia-enabled endpoint inside a browser (which defaults to accept:application/xhtml+xml,*/*), Spring HATEOAS will yield HAL.
This commit is contained in:
Greg Turnquist
2018-02-01 16:39:48 -06:00
parent eff893fcf8
commit dca65e15f8
13 changed files with 234 additions and 194 deletions

View File

@@ -30,10 +30,6 @@ import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlTransient;
import javax.xml.bind.annotation.XmlType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -47,7 +43,6 @@ import com.fasterxml.jackson.annotation.JsonInclude;
* @author Oliver Gierke
* @author Greg Turnquist
*/
@XmlType(name = "link", namespace = Link.ATOM_NAMESPACE)
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonIgnoreProperties(value = "templated", ignoreUnknown = true)
@AllArgsConstructor(access = AccessLevel.PACKAGE)
@@ -66,15 +61,15 @@ public class Link implements Serializable {
public static final String REL_NEXT = "next";
public static final String REL_LAST = "last";
private @XmlAttribute @Wither String rel;
private @XmlAttribute @Wither String href;
private @XmlAttribute @Wither String hreflang;
private @XmlAttribute @Wither String media;
private @XmlAttribute @Wither String title;
private @XmlAttribute @Wither String type;
private @XmlAttribute @Wither String deprecation;
private @XmlTransient @JsonIgnore UriTemplate template;
private @XmlTransient @JsonIgnore List<Affordance> affordances;
private @Wither String rel;
private @Wither String href;
private @Wither String hreflang;
private @Wither String media;
private @Wither String title;
private @Wither String type;
private @Wither String deprecation;
private @JsonIgnore UriTemplate template;
private @JsonIgnore List<Affordance> affordances;
/**
* Creates a new link to the given URI with the self rel.

View File

@@ -20,9 +20,6 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Optional;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -33,10 +30,9 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*
* @author Oliver Gierke
*/
@XmlRootElement(name = "pagedEntities")
public class PagedResources<T> extends Resources<T> {
public static PagedResources<?> NO_PAGE = new PagedResources<Object>();
public static PagedResources<?> NO_PAGE = new PagedResources<>();
private PageMetadata metadata;
@@ -169,10 +165,10 @@ public class PagedResources<T> extends Resources<T> {
*/
public static class PageMetadata {
@XmlAttribute @JsonProperty private long size;
@XmlAttribute @JsonProperty private long totalElements;
@XmlAttribute @JsonProperty private long totalPages;
@XmlAttribute @JsonProperty private long number;
@JsonProperty private long size;
@JsonProperty private long totalElements;
@JsonProperty private long totalPages;
@JsonProperty private long number;
protected PageMetadata() {

View File

@@ -18,9 +18,6 @@ package org.springframework.hateoas;
import java.util.Arrays;
import java.util.Collection;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonUnwrapped;
@@ -31,7 +28,6 @@ import com.fasterxml.jackson.annotation.JsonUnwrapped;
* @author Oliver Gierke
* @author Greg Turnquist
*/
@XmlRootElement
public class Resource<T> extends ResourceSupport {
private final T content;
@@ -73,7 +69,6 @@ public class Resource<T> extends ResourceSupport {
* @return the content
*/
@JsonUnwrapped
@XmlAnyElement
public T getContent() {
return content;
}

View File

@@ -21,8 +21,6 @@ import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonIgnore;
@@ -104,7 +102,6 @@ public class ResourceSupport implements Identifiable<Link> {
*
* @return
*/
@XmlElement(name = "link", namespace = Link.ATOM_NAMESPACE)
@JsonProperty("links")
public List<Link> getLinks() {
return links;

View File

@@ -21,10 +21,6 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import javax.xml.bind.annotation.XmlAnyElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonProperty;
@@ -34,7 +30,6 @@ import com.fasterxml.jackson.annotation.JsonProperty;
*
* @author Oliver Gierke
*/
@XmlRootElement(name = "entities")
public class Resources<T> extends ResourceSupport implements Iterable<T> {
private final Collection<T> content;
@@ -98,8 +93,6 @@ public class Resources<T> extends ResourceSupport implements Iterable<T> {
*
* @return the content will never be {@literal null}.
*/
@XmlAnyElement
@XmlElementWrapper
@JsonProperty("content")
public Collection<T> getContent() {
return Collections.unmodifiableCollection(content);

View File

@@ -20,11 +20,6 @@ import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -38,10 +33,8 @@ import com.fasterxml.jackson.annotation.JsonValue;
* @see https://github.com/blongden/vnd.error
* @author Oliver Gierke
*/
@XmlRootElement(name = "errors")
public class VndErrors implements Iterable<VndErrors.VndError> {
@XmlElement(name = "error")//
private final List<VndError> vndErrors;
/**
@@ -162,11 +155,10 @@ public class VndErrors implements Iterable<VndErrors.VndError> {
*
* @author Oliver Gierke
*/
@XmlType
public static class VndError extends ResourceSupport {
@XmlAttribute @JsonProperty private final String logref;
@XmlElement @JsonProperty private final String message;
@JsonProperty private final String logref;
@JsonProperty private final String message;
/**
* Creates a new {@link VndError} with the given logref, a message as well as some {@link Link}s.

View File

@@ -17,8 +17,6 @@ package org.springframework.hateoas.hal;
import java.util.List;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.ResourceSupport;
@@ -38,7 +36,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public abstract class ResourceSupportMixin extends ResourceSupport {
@Override
@XmlElement(name = "link")
@JsonProperty("_links")
@JsonInclude(Include.NON_EMPTY)
@JsonSerialize(using = Jackson2HalModule.HalLinkListSerializer.class)

View File

@@ -17,8 +17,6 @@ package org.springframework.hateoas.hal;
import java.util.Collection;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.hateoas.Resources;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -39,7 +37,6 @@ import com.fasterxml.jackson.databind.annotation.JsonSerialize;
public abstract class ResourcesMixin<T> extends Resources<T> {
@Override
@XmlElement(name = "embedded")
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
@JsonSerialize(using = Jackson2HalModule.HalResourcesSerializer.class)

View File

@@ -19,8 +19,6 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import javax.xml.bind.annotation.XmlElement;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.hateoas.Link;
@@ -89,7 +87,6 @@ public class Jackson2HalFormsModule extends SimpleModule {
abstract class ResourcesMixin<T> extends Resources<T> {
@Override
@XmlElement(name = "embedded")
@JsonProperty("_embedded")
@JsonInclude(Include.NON_EMPTY)
@JsonDeserialize(using = HalFormsResourcesDeserializer.class)

View File

@@ -1,82 +0,0 @@
/*
* Copyright 2013-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;
import static org.assertj.core.api.Assertions.*;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.StringReader;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.Charset;
import java.util.ArrayList;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.io.ClassPathResource;
/**
* Test for marshalling / unmarshalling of {@link PagedResources}.
*
* @author Eric Bottard
*/
public class PagedResourcesMarshallingTest {
Marshaller marshaller;
Unmarshaller unmarshaller;
String xmlReference;
PagedResources<Inner> pagedResources;
public PagedResourcesMarshallingTest() throws IOException {
xmlReference = readFile(new ClassPathResource("pagedresources.xml"));
}
@Before
public void setUp() throws Exception {
JAXBContext context = JAXBContext.newInstance(PagedResources.class);
marshaller = context.createMarshaller();
unmarshaller = context.createUnmarshaller();
pagedResources = new PagedResources<>(new ArrayList<>(), null);
}
/**
* @see #98
*/
@Test
public void jaxbUnMarshalling() throws Exception {
assertThat(unmarshaller.unmarshal(new StringReader(xmlReference))).isEqualTo(pagedResources);
}
public static class Inner {}
private static String readFile(org.springframework.core.io.Resource resource) throws IOException {
try (FileInputStream stream = new FileInputStream(resource.getFile())) {
FileChannel fc = stream.getChannel();
MappedByteBuffer bb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size());
return Charset.defaultCharset().decode(bb).toString();
}
}
}

View File

@@ -38,7 +38,6 @@ import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility;
public class ResourceIntegrationTest extends AbstractJackson2MarshallingIntegrationTest {
static final String REFERENCE = "{\"firstname\":\"Dave\",\"lastname\":\"Matthews\",\"links\":[{\"rel\":\"self\",\"href\":\"localhost\"}]}";
static final String XML_REFERENCE = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?><personResource xmlns:ns2=\"http://www.w3.org/2005/Atom\"><ns2:link href=\"/foo\" rel=\"bar\"/><person lastname=\"Matthews\" firstname=\"Dave\"/></personResource>";
@Test
public void inlinesContent() throws Exception {
@@ -53,29 +52,6 @@ public class ResourceIntegrationTest extends AbstractJackson2MarshallingIntegrat
assertThat(write(resource)).isEqualTo(REFERENCE);
}
/**
* @see #124
* @see #154
*/
@Test
public void marshalsResourceToXml() throws Exception {
Person person = new Person();
person.firstname = "Dave";
person.lastname = "Matthews";
PersonResource resource = new PersonResource(person);
resource.add(new Link("/foo", "bar"));
JAXBContext context = JAXBContext.newInstance(PersonResource.class, Person.class);
StringWriter writer = new StringWriter();
Marshaller marshaller = context.createMarshaller();
marshaller.marshal(resource, writer);
assertThat(new Diff(XML_REFERENCE, writer.toString()).similar()).isTrue();
}
/**
* @see #14
*/
@@ -90,7 +66,6 @@ public class ResourceIntegrationTest extends AbstractJackson2MarshallingIntegrat
assertThat(result.getContent().lastname).isEqualTo("Matthews");
}
@XmlRootElement
static class PersonResource extends Resource<Person> {
public PersonResource(Person person) {
@@ -101,10 +76,9 @@ public class ResourceIntegrationTest extends AbstractJackson2MarshallingIntegrat
}
@JsonAutoDetect(fieldVisibility = Visibility.ANY)
@XmlRootElement
static class Person {
@XmlAttribute String firstname;
@XmlAttribute String lastname;
String firstname;
String lastname;
}
}

View File

@@ -49,8 +49,6 @@ import com.fasterxml.jackson.databind.SerializationFeature;
public class VndErrorsMarshallingTest {
ObjectMapper jackson2Mapper;
Marshaller marshaller;
Unmarshaller unmarshaller;
RelProvider relProvider = new EvoInflectorRelProvider();
@@ -74,10 +72,6 @@ public class VndErrorsMarshallingTest {
jackson2Mapper.setHandlerInstantiator(new Jackson2HalModule.HalHandlerInstantiator(relProvider, null, null));
jackson2Mapper.configure(SerializationFeature.INDENT_OUTPUT, true);
JAXBContext context = JAXBContext.newInstance(VndErrors.class);
marshaller = context.createMarshaller();
unmarshaller = context.createUnmarshaller();
VndError error = new VndError("42", "Validation failed!", //
new Link("http://...", "describes"), new Link("http://...", "help"));
errors = new VndErrors(error, error, error);
@@ -91,18 +85,6 @@ public class VndErrorsMarshallingTest {
assertThat(jackson2Mapper.writeValueAsString(errors)).isEqualToIgnoringWhitespace(json2Reference);
}
/**
* @see #62, #154
*/
@Test
public void jaxbMarshalling() throws Exception {
Writer writer = new StringWriter();
marshaller.marshal(errors, writer);
assertThat(new Diff(xmlReference, writer.toString()).similar()).isTrue();
}
/**
* @see #93, #94
*/
@@ -111,15 +93,6 @@ public class VndErrorsMarshallingTest {
assertThat(jackson2Mapper.readValue(jsonReference, VndErrors.class)).isEqualTo(errors);
}
/**
* @see #93, #94
*/
@Test
public void jaxbUnMarshalling() throws Exception {
VndErrors actual = (VndErrors) unmarshaller.unmarshal(new StringReader(xmlReference));
assertThat(actual).isEqualTo(errors);
}
private static String readFile(org.springframework.core.io.Resource resource) throws IOException {
try (FileInputStream stream = new FileInputStream(resource.getFile())) {

View File

@@ -0,0 +1,216 @@
/*
* 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.hal;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.*;
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 static org.springframework.test.web.servlet.setup.MockMvcBuilders.*;
import lombok.Data;
import lombok.experimental.Wither;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.MediaTypes;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.Resources;
import org.springframework.hateoas.config.EnableHypermediaSupport;
import org.springframework.hateoas.config.EnableHypermediaSupport.HypermediaType;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
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;
/**
* @author Greg Turnquist
*/
@RunWith(SpringRunner.class)
@WebAppConfiguration
@ContextConfiguration
public class RenderHypermediaForDefaultAcceptHeadersTest {
@Autowired WebApplicationContext context;
MockMvc mockMvc;
@Before
public void setUp() {
this.mockMvc = webAppContextSetup(this.context).build();
}
/**
* Verify the default {@literal Accept} header used by a browser (which favors XML/HTML) still yields HAL-JSON.
*/
@Test
public void browserBasedDefaultAcceptHeadersShouldProduceHalJson() throws Exception {
this.mockMvc.perform(get("/employees").accept(MediaType.APPLICATION_XHTML_XML, MediaType.ALL)) //
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE));
}
/**
* Verify the default {@literal Accept} header used by cURL still yields HAL-JSON.
*/
@Test
public void curlBasedDefaultAcceptHeadersShouldProduceHalJson() throws Exception {
this.mockMvc.perform(get("/employees").accept(MediaType.ALL)) //
.andDo(print())
.andExpect(status().isOk())
.andExpect(header().string(HttpHeaders.CONTENT_TYPE, MediaTypes.HAL_JSON_UTF8_VALUE));
}
@RestController
static class EmployeeController {
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 Resources<Resource<Employee>> all() {
// Create a list of Resource<Employee>'s to return
List<Resource<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(EmployeeController.class).all()).withSelfRel();
// Return the collection of employee resources along with the composite affordance
return new Resources<>(employees, selfLink);
}
@GetMapping("/employees/{id}")
public Resource<Employee> findOne(@PathVariable Integer id) {
// Start the affordance with the "self" link, i.e. this method.
Link findOneLink = linkTo(methodOn(EmployeeController.class).findOne(id)).withSelfRel();
// Define final link as means to find entire collection.
Link employeesLink = linkTo(methodOn(EmployeeController.class).all()).withRel("employees");
// Return the affordance + a link back to the entire collection resource.
return new Resource<>(EMPLOYEES.get(id), findOneLink, employeesLink);
}
@PostMapping("/employees")
public ResponseEntity<?> newEmployee(@RequestBody Employee employee) {
int newEmployeeId = EMPLOYEES.size();
EMPLOYEES.put(newEmployeeId, employee);
try {
return ResponseEntity.noContent().location(new URI(findOne(newEmployeeId).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse("")))
.build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PutMapping("/employees/{id}")
public ResponseEntity<?> updateEmployee(@RequestBody Employee employee, @PathVariable Integer id) {
EMPLOYEES.put(id, employee);
try {
return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse("")))
.build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
@PatchMapping("/employees/{id}")
public ResponseEntity<?> partiallyUpdateEmployee(@RequestBody Employee employee, @PathVariable Integer id) {
Employee oldEmployee = EMPLOYEES.get(id);
Employee newEmployee = oldEmployee;
if (employee.getName() != null) {
newEmployee = newEmployee.withName(employee.getName());
}
if (employee.getRole() != null) {
newEmployee = newEmployee.withRole(employee.getRole());
}
EMPLOYEES.put(id, newEmployee);
try {
return ResponseEntity.noContent().location(new URI(findOne(id).getLink(Link.REL_SELF).map(link -> link.expand().getHref()).orElse("")))
.build();
} catch (URISyntaxException e) {
return ResponseEntity.badRequest().body(e.getMessage());
}
}
}
@Configuration
@EnableWebMvc
@EnableHypermediaSupport(type = { HypermediaType.HAL })
static class TestConfig {
@Bean
EmployeeController employeeController() {
return new EmployeeController();
}
}
@Data
@Wither
static class Employee {
private final String name;
private final String role;
}
}