A RESTful Smart Campus API built with JAX-RS (Jersey) for managing university rooms, sensors, and sensor readings.
- Author: Isuru Udara
- Student ID: w2120151 / 20240906
- Module: 5COSC022C — Client-Server Architectures
- University: University of Westminster
- Report: View Full Report (PDF)
- Java 11
- JAX-RS (Jakarta RESTful Web Services)
- Jersey 3.1.3 (JAX-RS Implementation)
- Apache Tomcat 10 (Embedded via Cargo)
- Jackson (JSON Serialisation)
- Maven (Build Tool)
- In-memory storage (HashMap / ArrayList)
- Java JDK 11+
- Apache Maven 3.6+
git clone https://github.com/iukumarasiri-dev/smart-campus-api.git
cd smart-campus-apimvn clean packagemvn clean package cargo:runhttp://localhost:8080/api/v1/
curl -X GET http://localhost:8080/api/v1/curl -X GET http://localhost:8080/api/v1/roomscurl -X POST http://localhost:8080/api/v1/rooms \
-H "Content-Type: application/json" \
-d '{"id": "GYM-201", "name": "Gymnasium", "capacity": 80}'curl -X GET http://localhost:8080/api/v1/rooms/LIB-301curl -X GET http://localhost:8080/api/v1/sensorscurl -X POST http://localhost:8080/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id": "TEMP-002", "type": "Temperature", "status": "ACTIVE", "currentValue": 21.0, "roomId": "LIB-301"}'curl -X GET "http://localhost:8080/api/v1/sensors?type=TEMPERATURE"curl -X GET http://localhost:8080/api/v1/sensors/TEMP-001/readingscurl -X POST http://localhost:8080/api/v1/sensors/TEMP-001/readings \
-H "Content-Type: application/json" \
-d '{"value": 24.5}'curl -X GET http://localhost:8080/api/v1/sensors/TEMP-001/readings/{readingId}curl -X DELETE http://localhost:8080/api/v1/sensors/TEMP-002curl -X DELETE http://localhost:8080/api/v1/rooms/GYM-201Question: In your report, explain the default lifecycle of a JAX-RS Resource class. Is a new instance instantiated for every incoming request, or does the runtime treat it as a singleton? Elaborate on how this architectural decision impacts the way you manage and synchronize your in-memory data structures (maps/lists) to prevent data loss or race conditions.
The Lifecycle
By default, JAX-RS uses a "per request" lifecycle. This means the server creates a brand new instance of a resource class (like RoomResource) for every single HTTP request, and then destroys it immediately after sending the response.
Managing State
Because these resource instances are destroyed so quickly, data cannot be saved directly inside them. If you stored a list of rooms in RoomResource, that list would be wiped clean on every single request. To solve this, the project uses a Singleton pattern via a DataStore class. This creates one permanent, shared "safe" for the data. All the temporary, per-request resource classes read and write to this single DataStore instance, preventing data loss.
private static final DataStore instance = new DataStore();
public static DataStore getInstance() { return instance; }Thread Safety Considerations
While the Singleton solves data loss, standard collections like HashMap are not thread-safe. If JAX-RS processes two POST requests at the exact same millisecond, concurrent writes could corrupt the map. In a production environment, we would use a ConcurrentHashMap and ArrayList with Collections.synchronizedList() or CopyOnWriteArrayList to fix this, but a standard HashMap is perfectly acceptable for the single-user testing scope.
Question: Why is the provision of "Hypermedia" (links and navigation within responses) considered a hallmark of advanced RESTful design (HATEOAS)? How does this approach benefit client developers compared to static documentation?
HATEOAS (Hypermedia As The Engine Of Application State) means an API response doesn't just return data; it also returns navigational links telling the client what it can do next.
HATEOAS is considered the highest maturity level of RESTful API design. It is a hallmark because it makes the API self-descriptive and self-navigating. Instead of simply returning raw, dead data, the server returns the data along with hypermedia links dictating the next possible actions (e.g., providing a link to "delete" a room right next to the room's data). This shifts the control of the application's state from the client to the server.
Benefits Over Static Documentation
- No Hardcoded URLs: Developers do not need to memorize or hardcode specific endpoint paths (like
/api/v1/rooms) into their applications. They simply look for the "rooms" link in the initial API response. - Resilience to Change: If backend developers change the routing structure, client applications will not break. The client automatically follows the newly updated links provided by the server.
- Always Accurate (Live Documentation): Static documentation easily becomes outdated as a project evolves. HATEOAS acts as "live" documentation; because the server generates the links dynamically, the available actions are always 100% accurate to the current system state.
Question: When returning a list of rooms, what are the implications of returning only IDs versus returning the full room objects? Consider network bandwidth and client-side processing.
Implications of Returning Full Objects
- Network Bandwidth: Every response carries all fields for every room (
id,name,capacity,sensorIds) in a single payload. At the current scale, this overhead is negligible. Even at a larger scale, becausesensorIdscontain short strings rather than deeply nested objects, the payload growth remains manageable. - Client-Side Processing: Low complexity. The client receives everything it needs in a single request, requiring no follow-up API calls to display room names, capacities, or sensor assignments.
Implications of Returning Only IDs
- Network Bandwidth: Returning only an array of ID strings (e.g.,
["LIB-301", "LAB-101"]) produces a much lighter response regardless of scale. - Client-Side Processing: High complexity and latency due to the N+1 Problem. The client must make a separate
GET /api/v1/rooms/{roomId}request for every ID to retrieve usable data. For a list of 50 rooms, this requires 51 total HTTP requests.
The current implementation makes the pragmatic choice. Because the Room model contains a small, bounded number of fields, returning full objects in the list response is preferable. Returning only IDs is generally only justified if the objects are massive or if clients rarely need the full details of every item at once.
Question: Is the DELETE operation idempotent in your implementation? Provide a detailed justification by describing what happens if a client mistakenly sends the exact same DELETE request for a room multiple times.
By definition, an idempotent operation must produce the same result no matter how many times it is called with the same input. No, the DELETE /api/v1/rooms/{roomId} operation in this implementation is not strictly idempotent.
Possible Outcomes on the First Call
Unlike a simple delete operation, the deleteRoom() method includes business rule preconditions, leading to three possible initial outcomes:
- Room does not exist → Returns
404 Not Found. - Room exists but has sensors assigned → Throws a
RoomNotEmptyException, returning409 Conflict. - Room exists and has no sensors → Deleted successfully, returning
200 OK.
Behavior Across Repeated Calls
Assuming the first call succeeds (200 OK), a second identical request will find that the room no longer exists and will return a 404 Not Found. Because the HTTP response code changes between calls (from 200 to 404), the operation is not strictly idempotent from the client's perspective.
RFC 9110 and Best Practices
The HTTP specification (RFC 9110) states that DELETE should be idempotent regarding server state (the room remains deleted). While some argue that returning a 404 on a repeat DELETE is acceptable since the desired end state (the room does not exist) is met, shifting response codes can cause issues. For example, if a client experiences a network timeout and retries, receiving a 404 might cause them to incorrectly assume the initial deletion failed. To make the implementation fully idempotent, repeated DELETE calls on an already deleted resource should ideally return 204 No Content.
Question: We explicitly use the @Consumes(MediaType.APPLICATION_JSON) annotation on the POST method. Explain the technical consequences if a client attempts to send data in a different format, such as text/plain or application/xml. How does JAX-RS handle this mismatch?
The Framework Gatekeeper & HTTP 415
When a POST request is sent to /api/v1/sensors, JAX-RS inspects the Content-Type header provided by the client and attempts to match it against the @Consumes annotation. If a client sends an unsupported format (like text/plain or application/xml), JAX-RS automatically intercepts and rejects the request, returning an HTTP 415 Unsupported Media Type status code.
Technical Consequences
This rejection happens at the framework's routing stage, before the resource method (createSensor()) ever executes. The technical benefits of this are significant:
- Zero Partial Processing: Because the method is never called, there is absolutely no risk of partial processing or data corruption within the
DataStore. - Prevention of Internal Errors: This acts as a strict contract enforcement gate. If JAX-RS allowed a
text/plainbody to reach the Jackson deserializer, Jackson would fail to parse it into aSensorobject and throw an exception, resulting in an unhelpful HTTP500 Internal Server Error. The@Consumesannotation prevents this crash entirely by validating the format upfront and returning a clean, standardized client error.
Question: You implemented this filtering using @QueryParam. Contrast this with an alternative design where the type is part of the URL path (e.g., /api/v1/sensors/type/CO2). Why is the query parameter approach generally considered superior for filtering and searching collections?
In REST architecture, the URL path identifies what resource is being accessed, while query parameters describe how that resource should be filtered or modified. The @QueryParam approach (GET /api/v1/sensors?type=CO2) is vastly superior to the path-based approach (GET /api/v1/sensors/type/CO2) for several architectural reasons:
| Design Concern | @QueryParam |
Path Segment |
|---|---|---|
| Resource Identity | Correctly identifies that type=CO2 is just a filter applied to the /sensors collection. |
Incorrectly implies that type/CO2 is a distinct, standalone sub-resource. |
| Optionality | Naturally optional. If omitted (/sensors), the API simply returns the unfiltered collection. |
Path segments are mandatory. Supporting an unfiltered list would require entirely separate route methods. |
| Composability | Easily scalable for multiple filters (e.g., ?type=CO2&status=ACTIVE). |
Becomes heavily ambiguous and difficult to route (e.g., /type/CO2/status/ACTIVE). |
| REST Semantics | The universally accepted standard for filtering, sorting, and searching. | Should be strictly reserved for identifying unique entities (e.g., /sensors/TEMP-001). |
To further improve usability, the filtering logic utilises .equalsIgnoreCase() when evaluating the query parameter. This means ?type=co2 and ?type=CO2 will return the exact same results, reducing the likelihood of clients receiving empty arrays due to mismatched letter casing.
Question: Discuss the architectural benefits of the Sub-Resource Locator pattern. How does delegating logic to separate classes help manage complexity in large APIs compared to defining every nested path (e.g., sensors/{id}/readings/{rid}) in one massive controller class?
Rather than defining every nested endpoint directly inside SensorResource, this implementation uses a Sub-Resource Locator method to delegate all /readings routing to a dedicated class:
@Path("/{sensorId}/readings")
public SensorReadingResource getReadingResource(@PathParam("sensorId") String sensorId) {
return new SensorReadingResource(sensorId);
}When JAX-RS encounters a request to /api/v1/sensors/{sensorId}/readings, it calls this method, receives the returned SensorReadingResource instance, and then scans that new class for its own @GET, @POST, and @Path annotations to complete the routing.
Delegating logic in this way provides five major architectural benefits:
1. The Single Responsibility Principle (SRP)
Each resource class is strictly limited to managing one entity. SensorResource manages sensors, and SensorReadingResource manages readings. If the readings logic requires modification (e.g., adding pagination or date filtering), only SensorReadingResource is touched. There is zero risk of accidentally breaking sensor creation or deletion logic because those operations are isolated in a completely separate file.
2. Controlled Context Passing
By extracting the sub-resource, the parent context (sensorId) is passed exactly once through the constructor:
public SensorReadingResource(String sensorId) {
this.sensorId = sensorId;
}Every endpoint method within SensorReadingResource automatically has access to the correct sensor context. Developers do not need to repeatedly declare, receive, and validate the sensorId as a @PathParam in every single method, drastically reducing boilerplate code.
3. Avoiding the "God Controller" Anti-Pattern
Without this pattern, a single controller class would be forced to define every endpoint across all levels of the resource hierarchy:
GET /sensorsPOST /sensorsGET /sensors/{id}GET /sensors/{id}/readingsPOST /sensors/{id}/readingsGET /sensors/{id}/readings/{rid}
As an API scales, this single class expands into thousands of lines, becoming incredibly difficult to read, maintain, and test. The Sub-Resource Locator pattern solves this by distributing responsibility across small, focused classes.
4. Independent Testability
Because SensorReadingResource is essentially a plain Java class with a constructor, it can be unit-tested in complete isolation. A test can instantiate it directly with a mock sensorId without needing to spin up the full JAX-RS runtime, configure HTTP servers, or mock routing contexts.
5. Additive Scalability
If the API later needs to support additional nested sub-resources such as /sensors/{id}/alerts or /sensors/{id}/calibrations, each new path simply receives its own dedicated resource class. The growth of the API becomes additive — new classes are introduced rather than an existing class being continuously expanded and modified.
Question: Why is HTTP 422 often considered more semantically accurate than a standard 404 when the issue is a missing reference inside a valid JSON payload?
When a client sends a POST request to /api/v1/sensors with a valid URL and valid JSON, but includes a roomId that does not exist, the correct HTTP status to return is 422 Unprocessable Entity.
The distinction between 404 and 422 is crucial for API clarity:
- HTTP 404 Not Found: The correct response when the requested URL or endpoint cannot be located (e.g.,
GET /api/v1/rooms/FAKE-999). It indicates the specific resource identified by the routing path is absent. - HTTP 422 Unprocessable Entity: Signals that the request was received, the URL is valid, the endpoint exists, and the JSON was successfully parsed — but the logical content of the payload is semantically invalid.
Using 404 in this scenario would be semantically misleading because the /sensors endpoint itself is perfectly valid and reachable. The issue is purely a data integrity violation within the request body. The LinkedResourceNotFoundExceptionMapper correctly returns 422 to clearly communicate that the request was understood, but the referenced entity within the payload could not be resolved.
Question: From a cybersecurity standpoint, explain the risks associated with exposing internal Java stack traces to external API consumers. What specific information could an attacker gather from such a trace?
Exposing raw Java stack traces in API responses is a significant security vulnerability, classified under OWASP A05: Security Misconfiguration. An attacker analyzing an exposed stack trace can extract several categories of highly sensitive information:
1. Technology Fingerprinting
A stack trace reveals the exact frameworks, libraries, and versions running on the server:
org.glassfish.jersey.server.ServerRuntime$Responder...
com.fasterxml.jackson.databind.exc.MismatchedInputException...
From this alone, an attacker knows the application is running Jersey and Jackson and can immediately search for known CVEs associated with those exact versions.
2. Internal Package and Class Structure
com.smartcampus.model.DataStore.getInstance
com.smartcampus.resource.RoomResource.deleteRoom
The attacker learns internal package names, class names, method names, and exact line numbers. This significantly lowers the effort required to craft targeted injection or exploitation attempts.
3. Business Logic Inference
Method names such as deleteRoom, createSensor, and getSensorIds expose internal workflows and data relationships that a well-designed API should keep completely opaque to external consumers.
Mitigation
The GlobalExceptionMapper mitigates all of these risks by catching every Throwable and logging full exception detail server-side only:
LOGGER.log(Level.SEVERE, "Unexpected error occurred", exception);This ensures the complete stack trace is recorded securely in the server logs for debugging purposes, while the client receives only a safe, generic 500 Internal Server Error message revealing nothing useful to a potential attacker.
Question: Why is it advantageous to use JAX-RS filters for cross-cutting concerns like logging, rather than manually inserting Logger.info() statements inside every single resource method?
| Problem | Consequence of Manual Logging |
|---|---|
| Code Duplication | The same log format must be copy-pasted into dozens of methods. Changing the format requires editing every single file. |
| Human Error | A developer adding a new endpoint can easily forget to include the logging statement. |
| Missed Logs | If a request is rejected before reaching a resource method (e.g., a 415 Unsupported Media Type error), no manual log will ever fire. |
| Mixed Concerns | Business logic (e.g., creating a room) becomes cluttered with infrastructure concerns (e.g., logging). |
The LoggingFilter class solves all of these problems by implementing both ContainerRequestFilter and ContainerResponseFilter, running automatically at the framework level for every incoming request and every outgoing response.
This provides massive advantages:
- Zero Additional Code: Adding a new endpoint automatically includes logging.
- Centralized Maintenance: Changing the log format requires editing only one class.
- Guaranteed Execution: Logging is guaranteed even for requests that fail framework-level validation and never reach the business logic.
- Separation of Concerns: Resource classes remain focused entirely on business logic, while the filter handles observability as a clean, independent, and reusable layer.