1. xml
  2. /web services
  3. /rest-xml

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

MethodPurposeSuccess CodesCommon Error Codes
GETRetrieve resource(s)200, 206404, 400
POSTCreate new resource201, 202400, 409, 422
PUTUpdate/replace resource200, 204400, 404, 422
DELETERemove resource200, 204404, 409
PATCHPartial update200, 204400, 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&amp;size=10"/>
        <link rel="next" href="/api/products?page=2&amp;size=10"/>
        <link rel="last" href="/api/products?page=16&amp;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