Smart Campus Sensor & Room Management API — JAX-RS RESTful service for managing campus rooms, sensors, and sensor readings. University of Westminster CSA Coursework (5COSC022W).
| Component | Technology |
|---|---|
| Language | Java 17 |
| Framework | JAX-RS (Jersey 3.1.5) |
| HTTP Server | Grizzly (embedded) |
| Serialization | Jackson |
| Build | Maven 3.9.6 (via wrapper) |
| Formatter | Spotless (Google Java Format) |
| Linter | Checkstyle |
| Git Hooks | .githooks/ (pre-commit + commit-msg) |
| Storage | In-memory (ConcurrentHashMap) |
smart-campus-api/
├── src/main/java/com/smartcampus/
│ ├── Main.java # Grizzly server entry point
│ ├── SmartCampusApplication.java # JAX-RS application config
│ ├── model/ # POJOs: Room, Sensor, SensorReading
│ ├── storage/ # In-memory data store
│ ├── resource/ # JAX-RS resource classes (REST endpoints)
│ ├── exception/ # Custom exceptions + ExceptionMappers
│ └── filter/ # Request/Response logging filters
├── config/checkstyle.xml # Checkstyle rules
├── .githooks/ # Git hooks (pre-commit, commit-msg)
├── .github/ # PR + issue templates
├── scripts/setup.sh # One-command project setup
└── pom.xml
Returns API metadata and available resource links.
| Method | Path | Description |
|---|---|---|
GET |
/rooms |
List all rooms |
POST |
/rooms |
Create a room |
GET |
/rooms/{id} |
Get room by ID |
PUT |
/rooms/{id} |
Update room |
DELETE |
/rooms/{id} |
Delete room (409 if sensors assigned) |
| Method | Path | Description |
|---|---|---|
GET |
/sensors |
List sensors (?type= filter) |
POST |
/sensors |
Create a sensor |
GET |
/sensors/{id} |
Get sensor by ID |
PUT |
/sensors/{id} |
Update sensor |
DELETE |
/sensors/{id} |
Delete sensor |
POST |
/sensors/{id}/readings |
Post a reading |
GET |
/sensors/{id}/readings |
Get readings for sensor |
# Clone and setup (configures git hooks, compiles project)
git clone https://github.com/Thanukamax/smart-campus-api.git
cd smart-campus-api
./scripts/setup.shOr manually:
git config core.hooksPath .githooks
./mvnw clean compile# Starts on http://localhost:8080
./mvnw exec:java# Format code (like Prettier)
./mvnw spotless:apply
# Check formatting
./mvnw spotless:check
# Run Checkstyle (like ESLint)
./mvnw checkstyle:checkThese run automatically via git hooks on every commit.
# 1. Discovery endpoint
curl -s http://localhost:8080/api/v1 | jq
# 2. Create a room
curl -s -X POST http://localhost:8080/api/v1/rooms \
-H "Content-Type: application/json" \
-d '{"id":"LIB-301","name":"Library Quiet Study","capacity":50}' | jq
# 3. List all rooms
curl -s http://localhost:8080/api/v1/rooms | jq
# 4. Get a specific room
curl -s http://localhost:8080/api/v1/rooms/LIB-301 | jq
# 5. Create a sensor linked to a room
curl -s -X POST http://localhost:8080/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id":"TEMP-001","type":"Temperature","status":"ACTIVE","roomId":"LIB-301"}' | jq
# 6. Create a CO2 sensor
curl -s -X POST http://localhost:8080/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id":"CO2-001","type":"CO2","status":"ACTIVE","roomId":"LIB-301"}' | jq
# 7. List all sensors
curl -s http://localhost:8080/api/v1/sensors | jq
# 8. Filter sensors by type
curl -s "http://localhost:8080/api/v1/sensors?type=Temperature" | jq
# 9. Post a sensor reading
curl -s -X POST http://localhost:8080/api/v1/sensors/TEMP-001/readings \
-H "Content-Type: application/json" \
-d '{"value":22.5}' | jq
# 10. Get reading history for a sensor
curl -s http://localhost:8080/api/v1/sensors/TEMP-001/readings | jq
# 11. Try deleting a room with sensors (expect 409 Conflict)
curl -s -X DELETE http://localhost:8080/api/v1/rooms/LIB-301 | jq
# 12. Try creating a sensor with non-existent room (expect 422)
curl -s -X POST http://localhost:8080/api/v1/sensors \
-H "Content-Type: application/json" \
-d '{"id":"TEMP-999","type":"Temperature","status":"ACTIVE","roomId":"FAKE-ROOM"}' | jqQ: 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?
By default, JAX-RS resource classes follow a per-request lifecycle. The JAX-RS runtime (Jersey, in our case) creates a new instance of the resource class for every incoming HTTP request. Once the request is served the instance is discarded and garbage-collected.
This has a direct impact on how we manage shared in-memory data. Because each request gets a fresh resource object, we cannot store data in instance fields of the resource class — those fields would be empty on the next request. Instead, we use a singleton DataStore (backed by ConcurrentHashMap) that is shared across all request-scoped resource instances. ConcurrentHashMap is thread-safe, which is critical because Grizzly's thread pool processes multiple requests concurrently. Without thread-safe collections, concurrent reads and writes could cause data loss or race conditions (e.g., a HashMap could corrupt its internal structure under concurrent modification).
Q: Why is the provision of "Hypermedia" (links and navigation within responses) considered a hallmark of advanced RESTful design (HATEOAS)?
HATEOAS (Hypermedia As The Engine Of Application State) means the API response itself tells the client what actions are available and where to find related resources, via embedded links. This is a hallmark of mature REST (Level 3 on the Richardson Maturity Model) because it decouples clients from hardcoded URL structures. Clients can discover and navigate the API dynamically by following links, rather than relying on out-of-band documentation that may become stale.
Compared to static documentation, HATEOAS has key advantages:
- Evolvability: The server can change URL structures without breaking clients, because clients follow links rather than constructing URLs.
- Self-description: New developers (or automated systems) can explore the API starting from a single root endpoint.
- Reduced coupling: Client logic depends on link relations (semantic names), not on specific URL patterns.
Our discovery endpoint at GET /api/v1 demonstrates this: it returns links like "rooms": "/api/v1/rooms" so clients know where to go next without hardcoding paths.
Q: When returning a list of rooms, what are the implications of returning only room IDs versus returning the full room objects?
Returning only IDs reduces payload size and network bandwidth — useful when the collection is large (thousands of rooms). However, it forces the client to make N additional GET /rooms/{id} requests to fetch details, creating a "chatty" API and causing the N+1 problem. This increases total latency and server load.
Returning full objects requires more bandwidth per response, but the client has everything it needs in a single request. For our campus API (hundreds to low thousands of rooms), the payload is manageable, so returning full objects is the pragmatic choice. It simplifies client-side code and reduces total round trips.
In production systems, a common middle ground is pagination (limit/offset or cursor-based) combined with sparse fieldsets (?fields=id,name), letting the client control the trade-off.
Q: Is the DELETE operation idempotent in your implementation?
Yes. Our DELETE /rooms/{roomId} is idempotent — calling it multiple times with the same room ID produces the same observable outcome:
- First call (room exists, no sensors): Room is deleted →
204 No Content. - Second call (room no longer exists): Returns
204 No Contentagain (no error). - Subsequent calls: Same
204 No Content.
The key is that we return 204 (not 404) when the room doesn't exist. This makes the operation safe to retry. If a client sends the same DELETE due to a network timeout or retry logic, no harm is done — the end state is the same. The only case that blocks deletion is the business constraint: if the room still has sensors, we return 409 Conflict regardless of how many times the request is sent, which is also idempotent (same input → same error).
Q: 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.
The @Consumes(MediaType.APPLICATION_JSON) annotation tells JAX-RS that this method only accepts requests with Content-Type: application/json. If a client sends a request with a different content type (e.g., text/plain, application/xml), JAX-RS will reject the request before it reaches the method body and return:
- HTTP 415 Unsupported Media Type — the standard status code for content negotiation failures.
This is handled entirely by the JAX-RS framework. The server never attempts to deserialize the payload; it fails fast at the routing/matching phase. This is important for security (prevents unexpected data from reaching business logic) and provides clear feedback to the client about what format is expected.
If no @Consumes annotation were present, Jersey would attempt to find a MessageBodyReader for whatever content type was sent, which could lead to confusing deserialization errors rather than a clean 415.
Q: Why is the query parameter approach generally considered superior for filtering and searching collections?
Using @QueryParam("type") on GET /api/v1/sensors?type=CO2 is preferred over path-based filtering (/api/v1/sensors/type/CO2) for several reasons:
-
Semantics: In REST, the path identifies a resource.
/sensors/type/CO2implies "CO2" is a sub-resource of "type", which is misleading — we're filtering a collection, not navigating a hierarchy. Query parameters are the standard mechanism for modifying how a collection is represented. -
Composability: Query parameters compose naturally:
?type=CO2&status=ACTIVE&page=2. With path segments, adding new filters changes the URL structure and makes routing complex. -
Optionality: Query parameters are inherently optional —
GET /sensorsreturns all,GET /sensors?type=CO2returns filtered. With path-based approaches, you need separate routes for filtered vs. unfiltered. -
Cacheability: Proxies and CDNs handle query parameters as cache keys by default. Changing the path structure can break caching strategies.
Q: Discuss the architectural benefits of the Sub-Resource Locator pattern.
The Sub-Resource Locator pattern (used in SensorResource.getReadingsSubResource()) delegates handling of a nested path to a separate class. Instead of SensorResource handling /sensors/{id}/readings and /sensors/{id}/readings/{rid} directly, it returns a SensorReadingResource instance that handles everything under /readings.
Benefits:
-
Separation of concerns: Each resource class has a single responsibility.
SensorResourcemanages sensors;SensorReadingResourcemanages readings. Neither class is bloated with methods for the other's domain. -
Scalability of codebase: In a large API with many nested paths (e.g.,
sensors/{id}/readings/{rid}/annotations), a single controller would become unmanageable. Sub-resource locators keep each class focused and testable. -
Reusability: The
SensorReadingResourceclass could potentially be reused or extended for different parent contexts without duplicating logic. -
Contextual construction: The locator method passes the
sensorIdto the sub-resource constructor, establishing the parent context cleanly. The sub-resource doesn't need to re-parse path parameters from the full URL.
Compared to one massive controller handling sensors/{id}/readings/{rid}, the sub-resource approach keeps routing logic close to the code that handles it, following the same principle as modular file structures in frontend frameworks.
Q: From a cybersecurity standpoint, explain the risks associated with exposing internal Java stack traces to external API consumers.
Exposing stack traces is a significant security risk (classified under OWASP's "Security Misconfiguration"). A stack trace reveals:
- Internal class names and package structure — reveals the architecture and frameworks used (e.g., Jersey, Grizzly), enabling targeted attacks against known vulnerabilities.
- File paths and line numbers — may expose the server's directory structure or deployment configuration.
- Database details — SQL exceptions can leak table names, column names, query structures, and even connection strings.
- Third-party library versions — attackers can look up known CVEs for the exact versions shown.
- Business logic flow — the call stack reveals how the application processes requests, helping attackers identify weak points.
Our GenericExceptionMapper catches all unhandled Throwable instances, logs the full stack trace server-side (for debugging), and returns only a generic "An unexpected error occurred" message to the client. This prevents information leakage while preserving observability for the development team.
Q: Why is it advantageous to use JAX-RS filters for cross-cutting concerns like logging?
JAX-RS filters (ContainerRequestFilter / ContainerResponseFilter) provide a centralized, declarative mechanism for cross-cutting concerns. The advantages over manual Logger.info() calls are:
-
DRY (Don't Repeat Yourself): One filter class handles logging for every endpoint. Without filters, you'd need to add logging statements to every resource method — dozens of places that must be kept in sync.
-
Separation of concerns: Resource methods focus purely on business logic. Logging, authentication, CORS headers, and other infrastructure concerns live in their own filter classes, keeping the codebase clean.
-
Consistency: A filter guarantees that every request and response is logged in the same format. Manual logging is error-prone — developers forget to add it, use inconsistent formats, or log at different severity levels.
-
Non-invasive: Filters are registered via
@Providerand picked up automatically by the JAX-RS runtime. Adding or removing logging requires zero changes to resource classes. -
Ordered pipeline: Multiple filters can be chained and ordered with
@Priority, creating a composable processing pipeline (e.g., logging → authentication → rate limiting → resource).
MIT