REST with XML
REST (Representational State Transfer) with XML provides a lightweight alternative to SOAP for web services. While JSON has become more popular, XML remains important for enterprise systems, legacy integration, and scenarios requiring rich data structures with validation.
REST Principles with XML
Resource-Based URLs
# Collection operations
GET /api/products # Get all products
POST /api/products # Create new product
PUT /api/products # Update all products (rare)
# Item operations
GET /api/products/12345 # Get specific product
PUT /api/products/12345 # Update specific product
DELETE /api/products/12345 # Delete specific product
# Nested resources
GET /api/products/12345/reviews # Get product reviews
POST /api/products/12345/reviews # Add product review
HTTP Methods and Status Codes
Method | Purpose | Success Codes | Common Error Codes |
---|---|---|---|
GET | Retrieve resource(s) | 200, 206 | 404, 400 |
POST | Create new resource | 201, 202 | 400, 409, 422 |
PUT | Update/replace resource | 200, 204 | 400, 404, 422 |
DELETE | Remove resource | 200, 204 | 404, 409 |
PATCH | Partial update | 200, 204 | 400, 404, 422 |
XML Data Representation
Product Resource Examples
<!-- Single Product Resource -->
<?xml version="1.0" encoding="UTF-8"?>
<product xmlns="http://api.example.com/catalog/v1"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<id>12345</id>
<name>Wireless Bluetooth Headphones</name>
<category>Electronics</category>
<description>High-quality wireless headphones with noise cancellation</description>
<price currency="USD">129.99</price>
<availability>
<inStock>true</inStock>
<quantity>45</quantity>
<warehouse>US-WEST-01</warehouse>
</availability>
<specifications>
<battery>30 hours</battery>
<range>10 meters</range>
<weight>250g</weight>
</specifications>
<links>
<link rel="self" href="/api/products/12345"/>
<link rel="reviews" href="/api/products/12345/reviews"/>
<link rel="related" href="/api/products?category=Electronics"/>
</links>
<metadata>
<created>2023-01-15T10:30:00Z</created>
<modified>2023-07-10T14:20:00Z</modified>
<version>3</version>
</metadata>
</product>
<!-- Product Collection Resource -->
<?xml version="1.0" encoding="UTF-8"?>
<products xmlns="http://api.example.com/catalog/v1">
<metadata>
<totalCount>156</totalCount>
<pageSize>10</pageSize>
<currentPage>1</currentPage>
<totalPages>16</totalPages>
</metadata>
<links>
<link rel="self" href="/api/products?page=1&size=10"/>
<link rel="next" href="/api/products?page=2&size=10"/>
<link rel="last" href="/api/products?page=16&size=10"/>
</links>
<items>
<product>
<id>12345</id>
<name>Wireless Bluetooth Headphones</name>
<category>Electronics</category>
<price currency="USD">129.99</price>
<links>
<link rel="self" href="/api/products/12345"/>
</links>
</product>
<product>
<id>12346</id>
<name>USB-C Cable</name>
<category>Accessories</category>
<price currency="USD">19.99</price>
<links>
<link rel="self" href="/api/products/12346"/>
</links>
</product>
<!-- More products... -->
</items>
</products>
Java REST Service Implementation
JAX-RS Resource Class
@Path("/products")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public class ProductResource {
@Inject
private ProductService productService;
@GET
public Response getProducts(
@QueryParam("page") @DefaultValue("1") int page,
@QueryParam("size") @DefaultValue("10") int size,
@QueryParam("category") String category,
@QueryParam("search") String searchQuery,
@Context UriInfo uriInfo) {
try {
ProductSearchCriteria criteria = new ProductSearchCriteria();
criteria.setPage(page);
criteria.setSize(size);
criteria.setCategory(category);
criteria.setSearchQuery(searchQuery);
ProductCollection result = productService.searchProducts(criteria);
// Add HATEOAS links
addCollectionLinks(result, uriInfo, criteria);
return Response.ok(result).build();
} catch (ValidationException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(createErrorResponse("VALIDATION_ERROR", e.getMessage()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(createErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
.build();
}
}
@GET
@Path("/{id}")
public Response getProduct(@PathParam("id") String productId, @Context UriInfo uriInfo) {
try {
Product product = productService.getProduct(productId);
if (product == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(createErrorResponse("PRODUCT_NOT_FOUND",
"Product with ID " + productId + " not found"))
.build();
}
// Add HATEOAS links
addProductLinks(product, uriInfo);
return Response.ok(product).build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(createErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
.build();
}
}
@POST
public Response createProduct(Product product, @Context UriInfo uriInfo) {
try {
// Validate input
validateProduct(product);
Product createdProduct = productService.createProduct(product);
// Add HATEOAS links
addProductLinks(createdProduct, uriInfo);
URI location = uriInfo.getAbsolutePathBuilder()
.path(createdProduct.getId())
.build();
return Response.created(location)
.entity(createdProduct)
.build();
} catch (ValidationException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(createErrorResponse("VALIDATION_ERROR", e.getMessage()))
.build();
} catch (DuplicateResourceException e) {
return Response.status(Response.Status.CONFLICT)
.entity(createErrorResponse("DUPLICATE_RESOURCE", e.getMessage()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(createErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
.build();
}
}
@PUT
@Path("/{id}")
public Response updateProduct(@PathParam("id") String productId,
Product product, @Context UriInfo uriInfo) {
try {
// Ensure ID consistency
product.setId(productId);
// Validate input
validateProduct(product);
Product updatedProduct = productService.updateProduct(product);
if (updatedProduct == null) {
return Response.status(Response.Status.NOT_FOUND)
.entity(createErrorResponse("PRODUCT_NOT_FOUND",
"Product with ID " + productId + " not found"))
.build();
}
// Add HATEOAS links
addProductLinks(updatedProduct, uriInfo);
return Response.ok(updatedProduct).build();
} catch (ValidationException e) {
return Response.status(Response.Status.BAD_REQUEST)
.entity(createErrorResponse("VALIDATION_ERROR", e.getMessage()))
.build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(createErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
.build();
}
}
@DELETE
@Path("/{id}")
public Response deleteProduct(@PathParam("id") String productId) {
try {
boolean deleted = productService.deleteProduct(productId);
if (!deleted) {
return Response.status(Response.Status.NOT_FOUND)
.entity(createErrorResponse("PRODUCT_NOT_FOUND",
"Product with ID " + productId + " not found"))
.build();
}
return Response.noContent().build();
} catch (Exception e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(createErrorResponse("INTERNAL_ERROR", "An unexpected error occurred"))
.build();
}
}
// Nested resource for product reviews
@Path("/{id}/reviews")
public ReviewResource getReviewResource(@PathParam("id") String productId) {
return new ReviewResource(productId, productService);
}
private void addProductLinks(Product product, UriInfo uriInfo) {
String selfUri = uriInfo.getAbsolutePathBuilder().build().toString();
product.addLink(new Link("self", selfUri));
String reviewsUri = uriInfo.getAbsolutePathBuilder()
.path("reviews").build().toString();
product.addLink(new Link("reviews", reviewsUri));
String categoryUri = uriInfo.getBaseUriBuilder()
.path("products")
.queryParam("category", product.getCategory())
.build().toString();
product.addLink(new Link("related", categoryUri));
}
private void addCollectionLinks(ProductCollection collection, UriInfo uriInfo,
ProductSearchCriteria criteria) {
UriBuilder baseBuilder = uriInfo.getAbsolutePathBuilder();
// Self link
URI selfUri = baseBuilder
.queryParam("page", criteria.getPage())
.queryParam("size", criteria.getSize())
.queryParam("category", criteria.getCategory())
.queryParam("search", criteria.getSearchQuery())
.build();
collection.addLink(new Link("self", selfUri.toString()));
// Next link
if (criteria.getPage() < collection.getMetadata().getTotalPages()) {
URI nextUri = baseBuilder.clone()
.queryParam("page", criteria.getPage() + 1)
.queryParam("size", criteria.getSize())
.queryParam("category", criteria.getCategory())
.queryParam("search", criteria.getSearchQuery())
.build();
collection.addLink(new Link("next", nextUri.toString()));
}
// Previous link
if (criteria.getPage() > 1) {
URI prevUri = baseBuilder.clone()
.queryParam("page", criteria.getPage() - 1)
.queryParam("size", criteria.getSize())
.queryParam("category", criteria.getCategory())
.queryParam("search", criteria.getSearchQuery())
.build();
collection.addLink(new Link("prev", prevUri.toString()));
}
}
private ErrorResponse createErrorResponse(String code, String message) {
ErrorResponse error = new ErrorResponse();
error.setCode(code);
error.setMessage(message);
error.setTimestamp(Instant.now());
return error;
}
private void validateProduct(Product product) throws ValidationException {
if (product.getName() == null || product.getName().trim().isEmpty()) {
throw new ValidationException("Product name is required");
}
if (product.getPrice() == null || product.getPrice().getAmount() == null) {
throw new ValidationException("Product price is required");
}
if (product.getPrice().getAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new ValidationException("Product price must be greater than zero");
}
if (product.getCategory() == null || product.getCategory().trim().isEmpty()) {
throw new ValidationException("Product category is required");
}
}
}
Data Transfer Objects
@XmlRootElement(name = "product")
@XmlAccessorType(XmlAccessType.FIELD)
public class Product {
@XmlElement(required = true)
private String id;
@XmlElement(required = true)
private String name;
@XmlElement(required = true)
private String category;
@XmlElement
private String description;
@XmlElement(required = true)
private Price price;
@XmlElement
private Availability availability;
@XmlElement
private Specifications specifications;
@XmlElementWrapper(name = "links")
@XmlElement(name = "link")
private List<Link> links = new ArrayList<>();
@XmlElement
private Metadata metadata;
// Constructors, getters, setters
public Product() {}
public void addLink(Link link) {
this.links.add(link);
}
// ... other methods
}
@XmlAccessorType(XmlAccessType.FIELD)
public class Price {
@XmlValue
private BigDecimal amount;
@XmlAttribute(required = true)
private String currency;
// Constructors, getters, setters
}
@XmlAccessorType(XmlAccessType.FIELD)
public class Link {
@XmlAttribute(required = true)
private String rel;
@XmlAttribute(required = true)
private String href;
@XmlAttribute
private String type = "application/xml";
public Link() {}
public Link(String rel, String href) {
this.rel = rel;
this.href = href;
}
// Getters and setters
}
@XmlRootElement(name = "products")
@XmlAccessorType(XmlAccessType.FIELD)
public class ProductCollection {
@XmlElement
private CollectionMetadata metadata;
@XmlElementWrapper(name = "links")
@XmlElement(name = "link")
private List<Link> links = new ArrayList<>();
@XmlElementWrapper(name = "items")
@XmlElement(name = "product")
private List<Product> products = new ArrayList<>();
public void addLink(Link link) {
this.links.add(link);
}
// Constructors, getters, setters
}
@XmlAccessorType(XmlAccessType.FIELD)
public class CollectionMetadata {
@XmlElement
private int totalCount;
@XmlElement
private int pageSize;
@XmlElement
private int currentPage;
@XmlElement
private int totalPages;
// Constructors, getters, setters
}
Error Response Handling
@XmlRootElement(name = "error")
@XmlAccessorType(XmlAccessType.FIELD)
public class ErrorResponse {
@XmlElement(required = true)
private String code;
@XmlElement(required = true)
private String message;
@XmlElement
private String details;
@XmlElement
@XmlJavaTypeAdapter(InstantAdapter.class)
private Instant timestamp;
@XmlElementWrapper(name = "validationErrors")
@XmlElement(name = "error")
private List<ValidationError> validationErrors;
// Constructors, getters, setters
}
@XmlAccessorType(XmlAccessType.FIELD)
public class ValidationError {
@XmlElement
private String field;
@XmlElement
private String message;
@XmlElement
private String rejectedValue;
// Constructors, getters, setters
}
public class InstantAdapter extends XmlAdapter<String, Instant> {
@Override
public Instant unmarshal(String v) throws Exception {
return Instant.parse(v);
}
@Override
public String marshal(Instant v) throws Exception {
return v.toString();
}
}
Content Negotiation
Multiple Format Support
@Path("/products")
public class ProductResource {
@GET
@Path("/{id}")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON, "text/html"})
public Response getProduct(@PathParam("id") String productId,
@Context HttpHeaders headers) {
Product product = productService.getProduct(productId);
if (product == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
// Check Accept header for content negotiation
MediaType requestedType = getPreferredMediaType(headers);
if (MediaType.TEXT_HTML_TYPE.isCompatible(requestedType)) {
// Return HTML representation
String html = generateProductHTML(product);
return Response.ok(html, MediaType.TEXT_HTML).build();
} else {
// Return XML or JSON based on Accept header (JAX-RS handles this automatically)
return Response.ok(product).build();
}
}
private MediaType getPreferredMediaType(HttpHeaders headers) {
List<MediaType> acceptableTypes = headers.getAcceptableMediaTypes();
for (MediaType type : acceptableTypes) {
if (MediaType.APPLICATION_XML_TYPE.isCompatible(type) ||
MediaType.APPLICATION_JSON_TYPE.isCompatible(type) ||
MediaType.TEXT_HTML_TYPE.isCompatible(type)) {
return type;
}
}
return MediaType.APPLICATION_XML_TYPE; // Default
}
private String generateProductHTML(Product product) {
return String.format(
"<html><head><title>%s</title></head>" +
"<body><h1>%s</h1>" +
"<p>Category: %s</p>" +
"<p>Price: %s %s</p>" +
"<p>%s</p></body></html>",
product.getName(),
product.getName(),
product.getCategory(),
product.getPrice().getCurrency(),
product.getPrice().getAmount(),
product.getDescription()
);
}
}
REST Client Implementation
JAX-RS Client
@Component
public class ProductRestClient {
private final Client client;
private final WebTarget baseTarget;
public ProductRestClient(@Value("${api.base.url}") String baseUrl) {
ClientConfig config = new ClientConfig();
config.property(ClientProperties.CONNECT_TIMEOUT, 5000);
config.property(ClientProperties.READ_TIMEOUT, 30000);
this.client = ClientBuilder.newClient(config);
this.baseTarget = client.target(baseUrl).path("api/products");
}
public Product getProduct(String productId) throws RestClientException {
try {
Response response = baseTarget
.path(productId)
.request(MediaType.APPLICATION_XML)
.get();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
return response.readEntity(Product.class);
} else if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()) {
return null;
} else {
ErrorResponse error = response.readEntity(ErrorResponse.class);
throw new RestClientException("Error getting product: " + error.getMessage());
}
} catch (Exception e) {
throw new RestClientException("Failed to get product", e);
}
}
public ProductCollection getProducts(int page, int size, String category)
throws RestClientException {
try {
WebTarget target = baseTarget
.queryParam("page", page)
.queryParam("size", size);
if (category != null) {
target = target.queryParam("category", category);
}
Response response = target
.request(MediaType.APPLICATION_XML)
.get();
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
return response.readEntity(ProductCollection.class);
} else {
ErrorResponse error = response.readEntity(ErrorResponse.class);
throw new RestClientException("Error getting products: " + error.getMessage());
}
} catch (Exception e) {
throw new RestClientException("Failed to get products", e);
}
}
public Product createProduct(Product product) throws RestClientException {
try {
Response response = baseTarget
.request(MediaType.APPLICATION_XML)
.post(Entity.entity(product, MediaType.APPLICATION_XML));
if (response.getStatus() == Response.Status.CREATED.getStatusCode()) {
return response.readEntity(Product.class);
} else {
ErrorResponse error = response.readEntity(ErrorResponse.class);
throw new RestClientException("Error creating product: " + error.getMessage());
}
} catch (Exception e) {
throw new RestClientException("Failed to create product", e);
}
}
public Product updateProduct(String productId, Product product) throws RestClientException {
try {
Response response = baseTarget
.path(productId)
.request(MediaType.APPLICATION_XML)
.put(Entity.entity(product, MediaType.APPLICATION_XML));
if (response.getStatus() == Response.Status.OK.getStatusCode()) {
return response.readEntity(Product.class);
} else if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()) {
return null;
} else {
ErrorResponse error = response.readEntity(ErrorResponse.class);
throw new RestClientException("Error updating product: " + error.getMessage());
}
} catch (Exception e) {
throw new RestClientException("Failed to update product", e);
}
}
public boolean deleteProduct(String productId) throws RestClientException {
try {
Response response = baseTarget
.path(productId)
.request()
.delete();
if (response.getStatus() == Response.Status.NO_CONTENT.getStatusCode()) {
return true;
} else if (response.getStatus() == Response.Status.NOT_FOUND.getStatusCode()) {
return false;
} else {
ErrorResponse error = response.readEntity(ErrorResponse.class);
throw new RestClientException("Error deleting product: " + error.getMessage());
}
} catch (Exception e) {
throw new RestClientException("Failed to delete product", e);
}
}
@PreDestroy
public void cleanup() {
if (client != null) {
client.close();
}
}
}
HTTP Client with XML Processing
public class HttpClientXMLProcessor {
private final HttpClient httpClient;
private final JAXBContext jaxbContext;
private final String baseUrl;
public HttpClientXMLProcessor(String baseUrl) throws JAXBException {
this.baseUrl = baseUrl;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5))
.build();
this.jaxbContext = JAXBContext.newInstance(Product.class, ProductCollection.class, ErrorResponse.class);
}
public Product getProduct(String productId) throws IOException, InterruptedException, JAXBException {
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api/products/" + productId))
.header("Accept", "application/xml")
.timeout(Duration.ofSeconds(30))
.GET()
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 200) {
return unmarshalXML(response.body(), Product.class);
} else if (response.statusCode() == 404) {
return null;
} else {
ErrorResponse error = unmarshalXML(response.body(), ErrorResponse.class);
throw new RuntimeException("Error: " + error.getMessage());
}
}
public Product createProduct(Product product) throws IOException, InterruptedException, JAXBException {
String xmlBody = marshalXML(product);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + "/api/products"))
.header("Content-Type", "application/xml")
.header("Accept", "application/xml")
.timeout(Duration.ofSeconds(30))
.POST(HttpRequest.BodyPublishers.ofString(xmlBody))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() == 201) {
return unmarshalXML(response.body(), Product.class);
} else {
ErrorResponse error = unmarshalXML(response.body(), ErrorResponse.class);
throw new RuntimeException("Error: " + error.getMessage());
}
}
private <T> T unmarshalXML(String xml, Class<T> clazz) throws JAXBException {
Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
StringReader reader = new StringReader(xml);
return clazz.cast(unmarshaller.unmarshal(reader));
}
private String marshalXML(Object object) throws JAXBException {
Marshaller marshaller = jaxbContext.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
StringWriter writer = new StringWriter();
marshaller.marshal(object, writer);
return writer.toString();
}
}
Testing REST XML Services
Integration Testing
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@TestPropertySource(locations = "classpath:application-test.properties")
public class ProductResourceIntegrationTest {
@Autowired
private TestRestTemplate restTemplate;
@LocalServerPort
private int port;
private String baseUrl;
@BeforeEach
public void setUp() {
baseUrl = "http://localhost:" + port + "/api/products";
}
@Test
public void testGetProduct_Success() {
// Given
String productId = "TEST_PRODUCT_1";
// When
ResponseEntity<Product> response = restTemplate.getForEntity(
baseUrl + "/" + productId, Product.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isEqualTo(productId);
assertThat(response.getHeaders().getContentType().toString())
.contains("application/xml");
}
@Test
public void testGetProduct_NotFound() {
// Given
String nonExistentId = "NON_EXISTENT";
// When
ResponseEntity<ErrorResponse> response = restTemplate.getForEntity(
baseUrl + "/" + nonExistentId, ErrorResponse.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getCode()).isEqualTo("PRODUCT_NOT_FOUND");
}
@Test
public void testCreateProduct_Success() {
// Given
Product newProduct = new Product();
newProduct.setName("Test Product");
newProduct.setCategory("Test Category");
newProduct.setDescription("Test Description");
newProduct.setPrice(new Price(new BigDecimal("99.99"), "USD"));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
HttpEntity<Product> request = new HttpEntity<>(newProduct, headers);
// When
ResponseEntity<Product> response = restTemplate.postForEntity(
baseUrl, request, Product.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isNotNull();
assertThat(response.getBody().getName()).isEqualTo("Test Product");
assertThat(response.getHeaders().getLocation()).isNotNull();
}
@Test
public void testCreateProduct_ValidationError() {
// Given - Invalid product (missing required fields)
Product invalidProduct = new Product();
invalidProduct.setDescription("Only description provided");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_XML);
headers.setAccept(Collections.singletonList(MediaType.APPLICATION_XML));
HttpEntity<Product> request = new HttpEntity<>(invalidProduct, headers);
// When
ResponseEntity<ErrorResponse> response = restTemplate.postForEntity(
baseUrl, request, ErrorResponse.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getCode()).isEqualTo("VALIDATION_ERROR");
}
@Test
public void testGetProducts_WithPagination() {
// When
ResponseEntity<ProductCollection> response = restTemplate.getForEntity(
baseUrl + "?page=1&size=5", ProductCollection.class);
// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getMetadata()).isNotNull();
assertThat(response.getBody().getMetadata().getPageSize()).isEqualTo(5);
assertThat(response.getBody().getProducts()).isNotNull();
assertThat(response.getBody().getLinks()).isNotEmpty();
}
}
Contract Testing with XML Schema
@Test
public void testXMLResponseValidation() throws Exception {
// Get XML response
String xmlResponse = restTemplate.getForObject(baseUrl + "/12345", String.class);
// Validate against XSD schema
SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
Schema schema = schemaFactory.newSchema(new File("src/test/resources/product-schema.xsd"));
Validator validator = schema.newValidator();
validator.validate(new StreamSource(new StringReader(xmlResponse)));
// If we get here, validation passed
assertThat(xmlResponse).contains("<product");
}
Performance Considerations
Caching Strategies
@Component
public class CachingProductService {
@Autowired
private ProductRepository productRepository;
@Cacheable(value = "products", key = "#productId")
public Product getProduct(String productId) {
return productRepository.findById(productId);
}
@CacheEvict(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
@CacheEvict(value = "products", key = "#productId")
public boolean deleteProduct(String productId) {
return productRepository.deleteById(productId);
}
}
HTTP Caching Headers
@GET
@Path("/{id}")
public Response getProduct(@PathParam("id") String productId, @Context Request request) {
Product product = productService.getProduct(productId);
if (product == null) {
return Response.status(Response.Status.NOT_FOUND).build();
}
// Create EntityTag based on product version/modification time
EntityTag etag = new EntityTag(String.valueOf(product.getVersion()));
// Check if-none-match header
Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
if (builder != null) {
// Client has current version, return 304 Not Modified
return builder.build();
}
// Return product with caching headers
return Response.ok(product)
.tag(etag)
.cacheControl(CacheControl.valueOf("max-age=300")) // 5 minutes
.lastModified(Date.from(product.getMetadata().getModified()))
.build();
}
Security Considerations
Input Validation
@Component
public class XMLSecurityHandler {
public void validateXMLInput(String xmlInput) throws SecurityException {
try {
// Disable external entity processing to prevent XXE attacks
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setExpandEntityReferences(false);
DocumentBuilder builder = factory.newDocumentBuilder();
Document doc = builder.parse(new ByteArrayInputStream(xmlInput.getBytes()));
// Additional validation can be added here
validateDocumentSize(doc);
validateElementDepth(doc.getDocumentElement(), 0, 10); // Max depth 10
} catch (Exception e) {
throw new SecurityException("Invalid XML input", e);
}
}
private void validateDocumentSize(Document doc) throws SecurityException {
// Limit document size to prevent DoS attacks
int elementCount = doc.getElementsByTagName("*").getLength();
if (elementCount > 1000) { // Adjust limit as needed
throw new SecurityException("XML document too large");
}
}
private void validateElementDepth(Element element, int currentDepth, int maxDepth)
throws SecurityException {
if (currentDepth > maxDepth) {
throw new SecurityException("XML document too deeply nested");
}
NodeList children = element.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child instanceof Element) {
validateElementDepth((Element) child, currentDepth + 1, maxDepth);
}
}
}
}
Best Practices
Resource Design
- Use nouns for resource URLs (
/products
not/getProducts
) - Use HTTP methods properly (GET for retrieval, POST for creation, etc.)
- Implement proper HTTP status codes
- Include HATEOAS links for discoverability
- Version your APIs (
/api/v1/products
)
XML Design
- Use meaningful element names
- Include proper namespaces
- Validate XML against schemas
- Provide comprehensive error responses
- Support content negotiation
Performance
- Implement caching at multiple levels
- Use compression for large responses
- Implement pagination for collections
- Consider async processing for heavy operations
Conclusion
REST with XML provides a lightweight, flexible approach to web services that leverages HTTP semantics and XML's structured data representation. While JSON has become more popular, XML remains valuable for enterprise systems requiring rich data structures, validation, and legacy integration.
Next Steps
- Explore RSS and Atom for syndication feeds
- Learn WSDL for service contracts
- Study Web Service Security for securing REST services