COIT20259 โ€” Week 11 Lab Tutorial

Building a RESTful Web Service with JAX-RS

๐Ÿ›  Apache NetBeans โ˜• GlassFish 7 ๐Ÿ“ฆ Jakarta EE 10 ๐Ÿ—„ MySQL โฑ Estimated time: 120โ€“150 min
Introduction

What You Will Build

A complete Jakarta EE RESTful web service that performs CRUD operations on a Product entity, backed by a MySQL database and deployed on GlassFish 7.

Unlike the Faces-based labs of Weeks 9โ€“10, this week there is no web UI. You will build a headless API that receives and returns JSON. Clients (browsers, mobile apps, frontend frameworks) consume the API over HTTP.

LO 1 JPA Entity & Persistence

  • Create a Product entity mapped to a MySQL table
  • Configure persistence.xml with JTA and auto-schema generation

LO 2 JAX-RS Application Config

  • Register the JAX-RS servlet with @ApplicationPath
  • Understand the URL formula: context root + app path + resource path

LO 3 CRUD Resource Endpoints

  • @GET, @POST, @PUT, @DELETE methods
  • @PathParam for resource identification
  • Response class for status codes (200, 201, 204, 404)

LO 4 Testing with curl

  • Send JSON payloads from the command line
  • Verify every CRUD operation end-to-end
  • Read HTTP status codes to confirm correct behaviour
Prerequisites
  • MySQL is running and accessible
  • GlassFish 7 is running โ€” http://localhost:4848 loads the Admin Console
  • A JDBC Connection Pool and JDBC Resource are configured in GlassFish (from earlier labs)
  • You understand JPA entities and persistence.xml from Weeks 1โ€“4
This lab is a fresh project โ€” not a copy of Week 10

RESTful services use a different architecture from Faces-based apps. Start with a brand-new Maven Web Application.

Setup

Create the Maven Web Application

Set up a new project in NetBeans, configure the Maven dependencies, and review the file structure you will build.

Create the Project

1
New Project

In NetBeans, go to:

Select Categories: Java with Maven โ†’ Projects: Web Application. Click Next.

2
Name and Location
FieldValue
Project NameProductRestService
Group Idau.edu.cqu
Packageau.edu.cqu.productrest

Click Next.

3
Server and Settings
FieldValue
ServerGlassFish Server
Java EE VersionJakarta EE 10 Web (or latest available)

Click Finish. NetBeans generates the project structure.

GlassFish not in the Server dropdown?

Add it first:

Point it to your GlassFish installation directory.

Configure pom.xml

4
Add the required dependencies

Open pom.xml from the Project Files node. Ensure these dependencies are present inside <dependencies>:

pom.xml โ€” dependenciesXML
<!-- Jakarta EE API โ€” GlassFish provides these at runtime -->
<dependency>
    <groupId>jakarta.platform</groupId>
    <artifactId>jakarta.jakartaee-api</artifactId>
    <version>10.0.0</version>
    <scope>provided</scope>
</dependency>

<!-- MySQL JDBC driver -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.33</version>
</dependency>
Why "provided" scope?

GlassFish already contains the Jakarta EE libraries (JAX-RS, JPA, CDI, JSON-B, etc.). The provided scope means "compile against this, but the server supplies it at runtime." This keeps your WAR file small. The MySQL connector does not use provided because GlassFish does not bundle it.

5
Build with Dependencies

Right-click the project โ†’

Watch the Output window โ€” Maven downloads any missing dependencies.

Files You Will Create in This Lab

ProductRestService/src/main/
  java/au/edu/cqu/productrest/
    Product.java
    RestApplication.java
    ProductResource.java
  resources/META-INF/
    persistence.xml
  webapp/
    WEB-INF/ (generated by NetBeans)
Notice โ€” no .xhtml pages

A RESTful web service has no user interface. No Facelets pages, no h:form, no managed beans. The only "views" are JSON responses returned over HTTP. Clients like curl, Postman, or a JavaScript frontend consume the API.

Task 1 โ€” JPA Entity & Persistence

Create the Product Entity and persistence.xml

Define a JPA entity that maps to a MySQL table. JPA auto-creates the table on first deployment โ€” no manual SQL required.

Create Product.java

1
Create the Java class

Right-click the package au.edu.cqu.productrest โ†’

Class Name: Product. Click Finish.

2
Replace the file contents

Delete everything NetBeans generated and paste the code below:

src/main/java/au/edu/cqu/productrest/Product.javaJava
package au.edu.cqu.productrest;

import jakarta.persistence.*;
import java.io.Serializable;

/**
 * JPA ENTITY โ€” each instance represents one row in the "products" table.
 *
 * @Entity              tells JPA "this is a database entity"
 * @Table(name=...)     maps it to the named table
 * @Id                  marks the primary key
 * @GeneratedValue      MySQL auto-increments the ID
 *
 * JSON-B (bundled with GlassFish) automatically converts this object
 * to/from JSON using the getter methods โ€” no extra annotations needed.
 */
@Entity
@Table(name = "products")
public class Product implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String name;

    private String description;

    @Column(nullable = false)
    private double price;

    // Default constructor โ€” required by JPA, never remove this
    public Product() {}

    // Getters and Setters โ€” JSON-B uses these for serialisation
    public Long getId()                       { return id; }
    public void setId(Long id)                { this.id = id; }

    public String getName()                   { return name; }
    public void setName(String name)          { this.name = name; }

    public String getDescription()            { return description; }
    public void setDescription(String d)     { this.description = d; }

    public double getPrice()                  { return price; }
    public void setPrice(double price)        { this.price = price; }
}
Why does JSON work without extra annotations?

GlassFish bundles Jakarta JSON Binding (JSON-B), which automatically converts Java objects to/from JSON using your getter methods. getName() becomes "name" in JSON. No @JsonProperty or manual parsing needed.

Create persistence.xml

3
Create the META-INF directory

Right-click src/main/resources โ†’

Name it META-INF.

4
Create the persistence.xml file

Right-click META-INF โ†’

Name it persistence. Click Finish. Replace the entire file with:

src/main/resources/META-INF/persistence.xmlXML
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="3.0"
  xmlns="https://jakarta.ee/xml/ns/persistence"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="https://jakarta.ee/xml/ns/persistence
    https://jakarta.ee/xml/ns/persistence/persistence_3_0.xsd">

  <persistence-unit name="ProductPU"
                     transaction-type="JTA">

    <!-- JNDI name MUST match your GlassFish JDBC Resource -->
    <jta-data-source>jdbc/myDatasource</jta-data-source>

    <class>au.edu.cqu.productrest.Product</class>

    <properties>
      <!-- Auto-create tables on first deployment -->
      <property name="jakarta.persistence.schema-generation.database.action"
                value="create"/>
    </properties>
  </persistence-unit>
</persistence>
Critical โ€” verify your JNDI name

The <jta-data-source> value must exactly match the JDBC Resource name in GlassFish Admin Console:

  1. Open http://localhost:4848
  2. Navigate to Resources โ†’ JDBC โ†’ JDBC Resources
  3. Copy the JNDI name (e.g. jdbc/myDatasource) and paste it into persistence.xml

If it doesn't match, JPA will fail with a "data source not found" error at deployment.

Schema generation: "create" vs "none"

"create" tells JPA to auto-create the products table on first deployment. After the table exists, change this to "none" to prevent data loss on redeployment. Use "drop-and-create" during development if you want a fresh table every time.

Task 2 โ€” JAX-RS Application Configuration

Register the REST Servlet with GlassFish

A single small Java class tells GlassFish "this project has REST endpoints" and defines the base URL path for all resources.

Create RestApplication.java

5
Create the class

Right-click the package au.edu.cqu.productrest โ†’

Class Name: RestApplication. Click Finish.

src/main/java/au/edu/cqu/productrest/RestApplication.javaJava
package au.edu.cqu.productrest;

import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application;

/**
 * JAX-RS APPLICATION CONFIG
 *
 * This class does two things with one annotation:
 *   1. Registers the JAX-RS servlet with GlassFish
 *   2. Sets "/api" as the base path for ALL REST endpoints
 *
 * After this, a resource at @Path("/products") becomes accessible at:
 *   http://localhost:8080/ProductRestService/api/products
 *         |__ context root __|__ app path __|__ resource __|
 *
 * No code needed inside โ€” the annotation does all the work.
 */
@ApplicationPath("/api")
public class RestApplication extends Application {
    // intentionally empty
}
The URL formula โ€” memorise this
PartSourceExample Value
Context RootProject name (set in GlassFish)/ProductRestService
Application Path@ApplicationPath("/api")/api
Resource Path@Path("/products")/products

Full URL: http://localhost:8080/ProductRestService/api/products

Task 3 โ€” GET Endpoints

Create the ProductResource with Read Operations

Build the REST resource class with two GET endpoints: list all products, and retrieve one by ID. Deploy and verify before moving on.

Create ProductResource.java

6
Create the class

Right-click the package โ†’

Class Name: ProductResource. Replace contents with:

src/main/java/au/edu/cqu/productrest/ProductResource.javaJava
package au.edu.cqu.productrest;

import jakarta.persistence.*;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;
import java.util.List;

/**
 * REST RESOURCE โ€” the equivalent of a Managed Bean in Faces, but instead
 * of rendering XHTML pages it returns JSON over HTTP.
 *
 * @Path("/products")  maps this class to /api/products
 * @Produces(JSON)     all methods return JSON by default
 * @Consumes(JSON)     all methods accept JSON input by default
 * @PersistenceContext  injects the JPA EntityManager
 */
@Path("/products")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
public class ProductResource {

    @PersistenceContext(unitName = "ProductPU")
    private EntityManager em;

    // โ”€โ”€ GET /api/products โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // Returns ALL products as a JSON array.
    // Empty table โ†’ returns []. HTTP status: 200 OK (default).
    @GET
    public List<Product> getAll() {
        return em.createQuery("SELECT p FROM Product p",
                              Product.class).getResultList();
    }

    // โ”€โ”€ GET /api/products/{id} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
    // Returns ONE product by primary key.
    // @PathParam extracts {id} from the URL.
    // Returns 404 if the ID does not exist.
    @GET
    @Path("/{id}")
    public Response getById(@PathParam("id") Long id) {
        Product p = em.find(Product.class, id);
        if (p == null) {
            return Response.status(404).build();
        }
        return Response.ok(p).build();
    }
}
Why use Response instead of just returning Product?

Returning Product directly always sends HTTP 200. The Response class lets you set the exact status code (200, 201, 204, 404). For getAll(), returning the list directly is fine because even an empty list is a valid 200. For getById(), you need to distinguish "found" (200) from "not found" (404).

Deploy and Test GET

7
Clean, Build, and Deploy
  1. Right-click the project โ†’
  2. Open GlassFish Admin Console โ†’ Applications โ†’ Undeploy any old version
  3. Right-click the project โ†’
  4. Watch the GlassFish Output tab for errors
8
Test in your browser

Open: http://localhost:8080/ProductRestService/api/products

You should see an empty JSON array: []

This is correct โ€” the table was just created and has no data yet.

Checkpoint โ€” verify before continuing
  • Browser shows [] (empty JSON array) โ€” not a 404 or error page
  • GlassFish Output shows "ProductRestService was successfully deployed"
  • No exceptions in the GlassFish server log
  • In MySQL, a products table now exists
Getting a 404?

Check these three things in order:

  1. RestApplication.java exists and has @ApplicationPath("/api")
  2. ProductResource.java has @Path("/products") at the class level
  3. The context root is correct โ€” check GlassFish Admin Console โ†’ Applications
Task 4 โ€” POST Endpoint (Create)

Add the Ability to Create Products

Add a POST method that accepts a JSON product in the request body, persists it to the database, and returns 201 Created.

Add the POST Method

9
Add this method inside ProductResource.java

Open ProductResource.java. Add the following method below the getById method:

ProductResource.java โ€” add the POST methodJava
// โ”€โ”€ POST /api/products โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Creates a new product from the JSON request body.
//
// JSON-B converts the incoming JSON into a Product object automatically.
// Example request body:
//   {"name":"Laptop","description":"A fast laptop","price":999.99}
//
// @Transactional wraps the method in a JTA transaction.
// Without this, em.persist() throws an error because there is
// no active transaction.
//
// Returns 201 Created with the saved product (including generated ID).
@POST
@Transactional
public Response create(Product product) {
    em.persist(product);
    return Response
        .status(Response.Status.CREATED)   // 201
        .entity(product)                   // includes the generated ID
        .build();
}
Don't forget the import

Add this import at the top of the file if it's not already there:

import jakarta.transaction.Transactional;

Without @Transactional, em.persist() fails with a 500 error because there is no active JTA transaction wrapping the database write.

Redeploy and Test with curl

10
Redeploy

Right-click the project โ†’ Clean and Build โ†’ Deploy.

11
Open a terminal and create a product

Open a command prompt (Windows) or terminal (Mac/Linux) and run:

Terminal โ€” create products with curlBash
# Create the first product
curl -X POST http://localhost:8080/ProductRestService/api/products ^
  -H "Content-Type: application/json" ^
  -d "{\"name\":\"Laptop\",\"description\":\"A fast laptop\",\"price\":999.99}"

# Create a second product
curl -X POST http://localhost:8080/ProductRestService/api/products ^
  -H "Content-Type: application/json" ^
  -d "{\"name\":\"Wireless Mouse\",\"description\":\"Ergonomic mouse\",\"price\":35.99}"

# Verify both were saved
curl http://localhost:8080/ProductRestService/api/products
Windows vs Mac/Linux curl syntax

Windows Command Prompt: Use ^ for line continuation and escape double quotes with \" inside the -d string (as shown above).

Mac/Linux Terminal: Use \ for line continuation and single quotes around the JSON body:
curl -X POST ... -d '{"name":"Laptop","description":"A fast laptop","price":999.99}'

Expected results
  • The POST returns the product with an auto-generated id field
  • The final GET returns a JSON array containing both products
Task 5 โ€” PUT & DELETE Endpoints

Complete the CRUD Operations

Add update and delete methods, then run an end-to-end test of all four CRUD operations.

Add the PUT Method (Update)

12
Add the PUT method to ProductResource.java

Add below the create method:

ProductResource.java โ€” PUT methodJava
// โ”€โ”€ PUT /api/products/{id} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Updates an existing product. ID from URL, new values from JSON body.
// Pattern: find โ†’ null check โ†’ copy fields โ†’ merge โ†’ respond.
// Returns 200 OK with updated product, or 404 if not found.
@PUT
@Path("/{id}")
@Transactional
public Response update(@PathParam("id") Long id,
                       Product updated) {
    Product existing = em.find(Product.class, id);
    if (existing == null) {
        return Response.status(404).build();
    }
    existing.setName(updated.getName());
    existing.setDescription(updated.getDescription());
    existing.setPrice(updated.getPrice());
    em.merge(existing);
    return Response.ok(existing).build();       // 200 OK
}

Add the DELETE Method

13
Add the DELETE method to ProductResource.java

Add below the update method:

ProductResource.java โ€” DELETE methodJava
// โ”€โ”€ DELETE /api/products/{id} โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// Removes a product by ID.
// Returns 204 No Content on success (no response body).
// Returns 404 if the ID does not exist.
@DELETE
@Path("/{id}")
@Transactional
public Response delete(@PathParam("id") Long id) {
    Product p = em.find(Product.class, id);
    if (p == null) {
        return Response.status(404).build();
    }
    em.remove(p);
    return Response.noContent().build();      // 204 No Content
}

Full CRUD Test Walkthrough

14
Redeploy and run the full test sequence

Right-click โ†’ Clean and Build โ†’ Deploy. Run these commands in order and verify each response:

#Operationcurl CommandStatusWhat to Check
1List allcurl .../api/products200Returns [] or existing products
2Createcurl -X POST ... -d '{"name":"Phone","price":699}'201Returns product with auto-generated id
3Read onecurl .../api/products/1200Returns the Phone product
4Updatecurl -X PUT .../1 -d '{"name":"Smartphone","price":749}'200Returns updated product
5Deletecurl -X DELETE .../api/products/1204No body returned
6Verifycurl .../api/products/1404Product no longer exists
How to check the HTTP status code in curl

Add -w "\n%{http_code}\n" to any curl command to print the status code after the response body.

All six tests passed?

Your RESTful CRUD service is fully working. Take a screenshot of your terminal output โ€” useful evidence for your portfolio.

Verification

End-to-End Testing Checklist

Every item must pass. Work through them in order.

Project Structure

  • persistence.xml is inside src/main/resources/META-INF/
  • <jta-data-source> matches the JDBC Resource in GlassFish
  • RestApplication.java extends Application with @ApplicationPath("/api")
  • ProductResource.java has @Path("/products")

GET Endpoints

  • GET /api/products โ†’ 200 with JSON array
  • GET /api/products/1 โ†’ 200 with single JSON object
  • GET /api/products/9999 โ†’ 404

POST Endpoint

  • POST with valid JSON โ†’ 201 Created with auto-generated id
  • POST without Content-Type: application/json โ†’ 415
  • Product appears in the MySQL products table

PUT Endpoint

  • PUT /api/products/1 with new data โ†’ 200 with updated product
  • PUT /api/products/9999 โ†’ 404
  • Subsequent GET returns the updated values

DELETE Endpoint

  • DELETE /api/products/1 โ†’ 204 No Content
  • DELETE /api/products/9999 โ†’ 404
  • After DELETE, GET /api/products/1 โ†’ 404

Final File Structure

ProductRestService/src/main/
  java/au/edu/cqu/productrest/
    Product.java โœ“ (JPA entity)
    RestApplication.java โœ“ (JAX-RS config)
    ProductResource.java โœ“ (CRUD endpoints)
  resources/META-INF/
    persistence.xml โœ“
Troubleshooting

Common Errors and How to Fix Them

Error / SymptomMost Likely CauseFix
404 Not Found on all /api/ URLs RestApplication.java is missing, doesn't extend Application, or @ApplicationPath is misspelled Verify the class exists, extends jakarta.ws.rs.core.Application, and has @ApplicationPath("/api"). Check the context root in GlassFish Admin Console โ†’ Applications.
500 Internal Server Error on deployment Persistence unit not found โ€” persistence.xml is in the wrong directory or JNDI name doesn't match Confirm persistence.xml is in src/main/resources/META-INF/ (not webapp/META-INF). Verify <jta-data-source> matches GlassFish. Ping the JDBC pool in Admin Console.
POST returns 415 Unsupported Media Type Missing Content-Type header in curl Add -H "Content-Type: application/json" to your curl POST/PUT commands.
POST returns 500 โ€” "no transaction is currently active" Missing @Transactional Add @Transactional to POST, PUT, DELETE methods. Import jakarta.transaction.Transactional.
JSON response shows empty {} Entity fields have no getters Ensure every field has a public getXxx() method. JSON-B ignores fields without getters.
Table not created in MySQL schema-generation property missing or set to "none" Set database.action to "create" in persistence.xml. Ensure the MySQL user has CREATE TABLE permission.
Deployment error: "not annotated with @Entity" Wrong import โ€” javax.persistence.Entity instead of jakarta.persistence.Entity Replace all javax.persistence.* with jakarta.persistence.*. GlassFish 7 uses the Jakarta namespace.
Browser shows XML instead of JSON Browser sends Accept: text/html by default This is cosmetic. Use a JSON formatter extension (e.g. JSONView) or test exclusively with curl.
PUT makes fields null JSON body is missing some fields โ€” JSON-B sets them to defaults Always include ALL fields in PUT request body. PUT replaces the entire resource.
Data reappears after redeploy Schema generation set to "drop-and-create" Expected with "drop-and-create". Change to "none" for persistence.
Extension Tasks

Going Further

Extension 1 โ€” Search with @QueryParam

Add filtering to GET /api/products. If the caller provides ?name=Laptop, return only products whose name contains that string (case-insensitive). If no parameter is provided, return all products.

@GET
public List<Product> getAll(@QueryParam("name") String name) {
    if (name != null && !name.isBlank()) {
        return em.createQuery(
            "SELECT p FROM Product p WHERE LOWER(p.name) LIKE :n",
            Product.class)
            .setParameter("n", "%" + name.toLowerCase() + "%")
            .getResultList();
    }
    return em.createQuery("SELECT p FROM Product p",
                          Product.class).getResultList();
}

Test: curl .../api/products?name=lap should return only "Laptop".

Extension 2 โ€” Add a Category Entity with @ManyToOne

Create a Category entity with id and name. Add a @ManyToOne relationship from Product to Category. Build a CategoryResource at @Path("/categories") with GET and POST endpoints.

Workflow: Create a category first, then create a product referencing the category ID.

Extension 3 โ€” Input Validation with Bean Validation

Add Jakarta Bean Validation annotations to Product:

  • @NotBlank on name
  • @Positive on price
  • @Size(min=2, max=100) on name

Then add @Valid before the Product parameter in POST and PUT methods. GlassFish returns 400 Bad Request when validation fails.

Test: curl -X POST ... -d '{"name":"","price":-5}' โ†’ 400.

Extension 4 โ€” CORS Filter for Frontend Clients

If a JavaScript frontend tries to call your API, the browser blocks it due to CORS restrictions. Create a JAX-RS ContainerResponseFilter:

@Provider
public class CorsFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext req,
                       ContainerResponseContext resp) {
        resp.getHeaders().add("Access-Control-Allow-Origin", "*");
        resp.getHeaders().add("Access-Control-Allow-Methods",
                              "GET, POST, PUT, DELETE, OPTIONS");
        resp.getHeaders().add("Access-Control-Allow-Headers",
                              "Content-Type");
    }
}

Test: Open a browser console and run:
fetch('http://localhost:8080/ProductRestService/api/products').then(r => r.json()).then(console.log)

COIT20259 Enterprise Computing โ€” Week 11 Lab Tutorial ยท Jakarta EE 10 / GlassFish 7 / MySQL / Apache NetBeans