Module: 5COSC022W — Client-Server Architectures
Technology Stack: Java 11 · JAX-RS (Jersey 2.40) · Grizzly HTTP Server · Jackson JSON
Base URL: http://localhost:9090/api/v1
- API Design Overview
- Project Structure
- Build & Run Instructions
- Sample curl Commands
- Conceptual Report — Question Answers
The Smart Campus API is a RESTful web service built with JAX-RS (Jersey) running on an embedded Grizzly HTTP server. It manages three primary entities:
| Entity | Path | Description |
|---|---|---|
| Room | /api/v1/rooms |
Physical campus rooms with capacity information |
| Sensor | /api/v1/sensors |
IoT sensors deployed inside rooms |
| SensorReading | /api/v1/sensors/{id}/readings |
Historical measurement log per sensor |
- In-memory storage uses
ConcurrentHashMap(thread-safe, no database required). - Referential integrity is enforced at the service layer: sensors must reference a valid room; rooms with sensors cannot be deleted.
- Versioned API at
/api/v1via@ApplicationPath. - Custom Exception Mappers ensure no Java stack trace ever leaks to clients.
- Sub-Resource Locator pattern cleanly delegates reading operations to a dedicated class.
- Cross-cutting logging is handled by a single JAX-RS filter, not scattered across resource methods.
GET /api/v1 Discovery & HATEOAS links
GET /api/v1/rooms List all rooms
POST /api/v1/rooms Create a room
GET /api/v1/rooms/{roomId} Get room by ID
DELETE /api/v1/rooms/{roomId} Delete room (fails if sensors present)
GET /api/v1/sensors List sensors (optional ?type= filter)
POST /api/v1/sensors Register a sensor
GET /api/v1/sensors/{sensorId} Get sensor by ID
DELETE /api/v1/sensors/{sensorId} Remove sensor
PATCH /api/v1/sensors/{sensorId}/status Update sensor status
GET /api/v1/sensors/{sensorId}/readings Sensor reading history
POST /api/v1/sensors/{sensorId}/readings Append a new reading
smart-campus-api/
├── pom.xml
└── src/main/java/com/smartcampus/
├── Main.java # Grizzly server bootstrap
├── SmartCampusApplication.java # @ApplicationPath("/api/v1")
├── DataStore.java # Thread-safe in-memory singleton
├── model/
│ ├── Room.java
│ ├── Sensor.java
│ └── SensorReading.java
├── resource/
│ ├── DiscoveryResource.java # GET /api/v1
│ ├── RoomResource.java # /api/v1/rooms
│ ├── SensorResource.java # /api/v1/sensors
│ └── SensorReadingResource.java # /api/v1/sensors/{id}/readings (sub-resource)
├── exception/
│ ├── RoomNotEmptyException.java
│ ├── RoomNotEmptyExceptionMapper.java # 409 Conflict
│ ├── LinkedResourceNotFoundException.java
│ ├── LinkedResourceNotFoundExceptionMapper.java # 422 Unprocessable Entity
│ ├── SensorUnavailableException.java
│ ├── SensorUnavailableExceptionMapper.java # 403 Forbidden
│ └── GlobalExceptionMapper.java # 500 catch-all
└── filter/
└── ApiLoggingFilter.java # Request + response logging
- Java 11 or higher
- Apache Maven 3.6 or higher
git clone https://github.com/<your-username>/smart-campus-api.git
cd smart-campus-apimvn clean packageThis produces target/smart-campus-api-1.0-SNAPSHOT.jar — a self-contained executable JAR with Grizzly and Jersey bundled inside.
java -jar target/smart-campus-api-1.0-SNAPSHOT.jarThe server starts on port 9090. You will see:
Smart Campus API started at http://0.0.0.0:9090/
Discovery endpoint: http://localhost:9090/api/v1
Press ENTER to stop the server...
curl http://localhost:9090/api/v1Two demo objects are pre-loaded: Room
LIB-301with sensorTEMP-001, and RoomLAB-101with sensorCO2-001.
curl -s http://localhost:9090/api/v1 | python3 -m json.toolExpected response:
{
"api": "Smart Campus Sensor & Room Management API",
"version": "1.0.0",
"contact": { "name": "Smart Campus Admin", "email": "admin@smartcampus.ac.uk" },
"resources": {
"rooms": "/api/v1/rooms",
"sensors": "/api/v1/sensors"
}
}curl -s -X POST http://localhost:9090/api/v1/rooms \
-H "Content-Type: application/json" \
-d '{"id":"HALL-200","name":"Main Lecture Hall","capacity":200}' \
| python3 -m json.toolExpected: 201 Created with the room object.
curl -s -X POST http://localhost:9090/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id":"OCC-007","type":"Occupancy","status":"ACTIVE","currentValue":0,"roomId":"HALL-200"}' \
| python3 -m json.toolExpected: 201 Created with the sensor object.
curl -s -X POST http://localhost:9090/api/v1/sensors/TEMP-001/readings \
-H "Content-Type: application/json" \
-d '{"value":23.4}' \
| python3 -m json.toolExpected: 201 Created with the reading (UUID + epoch timestamp + value).
The parent sensor's currentValue is simultaneously updated to 23.4.
curl -s "http://localhost:9090/api/v1/sensors?type=CO2" | python3 -m json.toolExpected: array containing only sensors whose type equals CO2.
curl -s -X DELETE http://localhost:9090/api/v1/rooms/LIB-301 | python3 -m json.toolExpected: 409 Conflict
{
"status": 409,
"error": "Conflict",
"message": "Room 'LIB-301' still has active sensors assigned and cannot be deleted.",
"hint": "Remove or reassign all sensors from this room before deleting it."
}curl -s -X POST http://localhost:9090/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id":"BAD-001","type":"Temperature","roomId":"GHOST-999"}' \
| python3 -m json.toolExpected: 422 Unprocessable Entity
# Step 1: set sensor to MAINTENANCE
curl -s -X PATCH http://localhost:9090/api/v1/sensors/TEMP-001/status \
-H "Content-Type: application/json" \
-d '{"status":"MAINTENANCE"}'
# Step 2: try posting a reading — will be rejected
curl -s -X POST http://localhost:9090/api/v1/sensors/TEMP-001/readings \
-H "Content-Type: application/json" \
-d '{"value":19.9}' | python3 -m json.toolExpected: 403 Forbidden
# Remove the sensor from LIB-301 first
curl -s -X DELETE http://localhost:9090/api/v1/sensors/TEMP-001
# Now delete the room
curl -s -X DELETE http://localhost:9090/api/v1/rooms/LIB-301 | python3 -m json.toolExpected: 200 OK with confirmation message.
Q: What is the default lifecycle of a JAX-RS Resource class? How does this impact management of in-memory data structures?
By default, JAX-RS creates a new instance of each resource class for every incoming HTTP request (per-request scope). This is deliberate: it guarantees that each request starts with a clean, isolated object and eliminates the risk of one request's local state bleeding into another.
The architectural consequence is important: because the resource class object is discarded after each request, any instance fields declared on the resource class are lost when the request completes. If the rooms map were stored as a field on RoomResource, every GET /rooms would see an empty map, because the previous request's instance — and its data — no longer exists.
To solve this, all shared mutable state lives in DataStore, a JVM-level singleton (private constructor, static INSTANCE). Every request, regardless of which resource-class instance handles it, calls DataStore.getInstance() and operates on the same maps. ConcurrentHashMap is used instead of HashMap because multiple threads may read and write simultaneously (one thread per request). ConcurrentHashMap provides thread-safe atomic operations (put, get, computeIfAbsent) without requiring synchronized blocks on every method, preventing data loss and race conditions at scale.
Q: Why is Hypermedia (HATEOAS) considered a hallmark of advanced RESTful design? How does it benefit client developers?
HATEOAS (Hypermedia As The Engine Of Application State) means that an API response includes links to related actions and resources, not just data. Rather than a client needing to hard-code URLs like /api/v1/rooms, it discovers them dynamically from the API's own responses.
The benefits for client developers are significant. First, client code is decoupled from URL structure — if the server reorganises its paths, clients that follow links rather than constructing URLs are unaffected. Second, discoverability is built in: a developer hitting /api/v1 immediately learns what the API can do without consulting external documentation. Third, the API can guide clients through valid workflows by only advertising links for actions that are currently permitted (e.g., only including a "delete" link if the room is empty), reducing the likelihood of clients sending invalid requests. The discovery endpoint implemented in DiscoveryResource is a foundational step toward HATEOAS by advertising the primary resource collections in its response.
Q: What are the implications of returning only IDs versus returning full room objects in a list response?
Returning only IDs minimises the payload size and is appropriate when clients will typically select one item for detail — they make a cheap list call, then one targeted GET /{id} call. This is the "lazy loading" pattern and works well when the list is large or fields are expensive to compute.
Returning full objects means the client can render a complete view with a single HTTP round-trip, which reduces latency and simplifies client logic. The trade-off is increased bandwidth and serialisation cost when the list is large or fields are numerous.
This API returns full objects, which is the more pragmatic choice for a campus management dashboard where operators need to see name and capacity at a glance, not just an ID. For very large collections, pagination (e.g., ?page=1&size=20) would be the next architectural step to combine completeness with efficiency.
Q: Is DELETE idempotent in your implementation? Justify what happens on repeated DELETE requests.
Yes, DELETE is idempotent in the sense that repeated calls leave the server in the same final state: the room does not exist. However, the HTTP response code differs between calls.
- First call: The room exists, the safety check passes (no sensors), the room is removed from the
ConcurrentHashMap, and the server responds200 OKwith a confirmation message. - Second (and subsequent) calls:
store.getRoom(roomId)returnsnull— the room is already gone. The method returns404 Not Found.
The server state is identical after both calls (room absent), which satisfies the RFC definition of idempotency. The varying status code is expected and correct: the resource's absence is a meaningful fact, and 404 honestly represents it. This differs from PUT, where repeated identical requests always return 200, because PUT is creating/replacing a resource that can always be written again. A client that repeats a DELETE due to a network timeout should gracefully handle both 200 and 404 as success indicators.
Q: What happens if a client sends data in a format other than application/json to a method annotated with @Consumes(APPLICATION_JSON)?
When a resource method is annotated with @Consumes(MediaType.APPLICATION_JSON), JAX-RS uses this as a contract: it will only route the request to this method if the incoming Content-Type header matches application/json.
If a client sends Content-Type: text/plain or Content-Type: application/xml, JAX-RS cannot find a MessageBodyReader capable of deserialising that media type into the target Java type. The framework rejects the request before the method body is ever invoked and returns HTTP 415 Unsupported Media Type. This is handled entirely by the JAX-RS runtime — no application code is needed to enforce it. This is one of the key advantages of declarative content negotiation: the developer expresses intent via annotations and the framework enforces it consistently.
Q: Why is the query parameter approach (GET /sensors?type=CO2) generally considered superior to a path segment (/sensors/type/CO2) for filtering?
The distinction matters semantically. A path segment represents a distinct, addressable resource: /sensors/CO2 implies CO2 is a specific named resource, which is misleading and would suggest it has its own identity, sub-resources, and can be individually manipulated with PUT or DELETE. It also makes the URL ambiguous — is CO2 a sensor ID or a type?
A query parameter correctly communicates that the base collection (/sensors) is being filtered or searched according to an optional criterion. This aligns with REST conventions: the path identifies the resource collection, and query parameters refine the representation returned. Multiple filters compose naturally (?type=CO2&status=ACTIVE), whereas path-based filtering leads to combinatorial explosion of paths. Query parameters are also optional by nature — omitting ?type= returns all sensors — whereas a mandatory path segment would require a separate "unfiltered" route. Practically, @QueryParam in JAX-RS handles null automatically when the parameter is absent, making the implementation concise and correct.
Q: What are the architectural benefits of the Sub-Resource Locator pattern?
The Sub-Resource Locator pattern delegates requests for a nested path to a dedicated class by returning an instance of that class from a method annotated only with @Path (no HTTP verb annotation). JAX-RS then dispatches the actual HTTP method to the returned object.
The benefits in a large API are substantial. Separation of concerns is the primary gain: SensorResource is responsible for sensor-level operations; SensorReadingResource is solely responsible for reading operations. Neither class needs awareness of the other's internal logic. Maintainability improves because each class is smaller and independently testable — unit tests for readings do not need to instantiate the full sensor resource. Scalability of the codebase is also improved: as the API grows, new sub-resources (e.g., /{sensorId}/alerts) can be added by creating a new class and a single locator method, without touching existing classes. Contrast this with a "monolithic controller" approach where every nested path (/sensors/{id}/readings, /sensors/{id}/readings/{rid}) is crammed into one class — that class quickly becomes unmanageable, violates the Single Responsibility Principle, and makes merge conflicts inevitable in team environments.
Q: Why is HTTP 422 often more semantically accurate than 404 when the issue is a missing reference inside a valid JSON payload?
404 Not Found means the requested URL does not identify an existing resource. If a client sends POST /api/v1/sensors with a body referencing roomId: "GHOST-999", the URL /api/v1/sensors is perfectly valid — the sensors collection exists and responded to the request. Returning 404 would be misleading because it suggests the endpoint itself was not found.
422 Unprocessable Entity means the server understood the request's content type and syntax (valid JSON), but the semantic content is invalid. The body is well-formed, but a value within it refers to a resource that does not exist — this is a business-logic validation failure, not a routing failure. 422 precisely communicates: "I understood what you sent; I simply cannot act on it because a dependency is missing." This distinction helps client developers diagnose errors correctly: 404 would send them checking their URL, while 422 directs them to inspect their request body.
Q: From a cybersecurity standpoint, what risks are associated with exposing Java stack traces to external API consumers?
Exposing raw stack traces to external clients is a significant information-disclosure vulnerability with several concrete attack vectors:
Technology fingerprinting: A stack trace immediately reveals the exact framework (jersey-server-2.40.0), Java version, and application server in use. An attacker can then search for known CVEs (Common Vulnerabilities and Exposures) for those exact versions.
Internal path and package disclosure: Stack frames expose the application's full package structure (com.smartcampus.resource.SensorResource.createSensor:47), which reveals internal architecture and helps an attacker understand the codebase to craft targeted exploits.
Business logic inference: Repeated probing that triggers different exceptions reveals branching logic — for example, discovering that line 47 throws a NullPointerException reveals an unguarded code path that can potentially be exploited to crash the service or bypass validation.
Third-party library exposure: Stack traces often include frames from libraries (Jackson, Grizzly), revealing exact dependency versions and enabling supply-chain attack targeting.
The GlobalExceptionMapper in this project addresses all of these by logging the full detail server-side only (accessible to authorised operators) while returning a generic, information-free 500 message to the client.
Q: Why is it advantageous to use JAX-RS filters for logging rather than manually inserting Logger.info() statements in every resource method?
Logging is a cross-cutting concern — it applies uniformly across all resources and has nothing to do with the business logic of managing rooms or sensors. Inserting Logger.info() manually into every method has several drawbacks:
Code duplication: With dozens of endpoints, the same logging boilerplate appears in every method. If the log format changes (e.g., adding a correlation ID), every method must be updated.
Violation of separation of concerns: Resource methods should express business logic. Mixing in infrastructure concerns makes methods harder to read, test, and reason about.
Risk of omission: A developer adding a new endpoint may forget to add the logging call, creating invisible blind spots in production observability.
A JAX-RS filter registered with @Provider is applied automatically to every request and response without any per-method code. The ApiLoggingFilter in this project implements both ContainerRequestFilter and ContainerResponseFilter in a single class, providing complete request-in/response-out observability with zero coupling to business logic. New endpoints added in the future are automatically covered.
Report prepared for 5COSC022W Coursework — Smart Campus API (2025/26)