Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Smart Campus Sensor & Room Management API

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


Table of Contents

  1. API Design Overview
  2. Project Structure
  3. Build & Run Instructions
  4. Sample curl Commands
  5. Conceptual Report — Question Answers

API Design Overview

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

Key Design Decisions

  • 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/v1 via @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.

Endpoint Summary

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

Project Structure

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

Build & Run Instructions

Prerequisites

  • Java 11 or higher
  • Apache Maven 3.6 or higher

Step 1 — Clone the repository

git clone https://github.com/<your-username>/smart-campus-api.git
cd smart-campus-api

Step 2 — Build the fat JAR

mvn clean package

This produces target/smart-campus-api-1.0-SNAPSHOT.jar — a self-contained executable JAR with Grizzly and Jersey bundled inside.

Step 3 — Run the server

java -jar target/smart-campus-api-1.0-SNAPSHOT.jar

The 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...

Step 4 — Verify it is running

curl http://localhost:9090/api/v1

Sample curl Commands

Two demo objects are pre-loaded: Room LIB-301 with sensor TEMP-001, and Room LAB-101 with sensor CO2-001.

1 — Discovery endpoint

curl -s http://localhost:9090/api/v1 | python3 -m json.tool

Expected 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"
  }
}

2 — Create a new room

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.tool

Expected: 201 Created with the room object.


3 — Register a sensor in an existing room

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.tool

Expected: 201 Created with the sensor object.


4 — Post a sensor reading (triggers currentValue update)

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.tool

Expected: 201 Created with the reading (UUID + epoch timestamp + value).
The parent sensor's currentValue is simultaneously updated to 23.4.


5 — Filter sensors by type

curl -s "http://localhost:9090/api/v1/sensors?type=CO2" | python3 -m json.tool

Expected: array containing only sensors whose type equals CO2.


6 — Attempt to delete a room that still has sensors (409 error demo)

curl -s -X DELETE http://localhost:9090/api/v1/rooms/LIB-301 | python3 -m json.tool

Expected: 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."
}

7 — Attempt to POST a sensor with an invalid roomId (422 error demo)

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.tool

Expected: 422 Unprocessable Entity


8 — Put a sensor into MAINTENANCE then attempt a reading (403 error demo)

# 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.tool

Expected: 403 Forbidden


9 — Successfully delete a room after removing its sensor

# 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.tool

Expected: 200 OK with confirmation message.


Conceptual Report — Question Answers


Part 1.1 — JAX-RS Resource Class Lifecycle

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.


Part 1.2 — HATEOAS

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.


Part 2.1 — IDs vs Full Objects in List Responses

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.


Part 2.2 — Idempotency of DELETE

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 responds 200 OK with a confirmation message.
  • Second (and subsequent) calls: store.getRoom(roomId) returns null — the room is already gone. The method returns 404 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.


Part 3.1 — @Consumes and Content-Type Mismatch

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.


Part 3.2 — @QueryParam vs Path Segment for Filtering

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.


Part 4.1 — Sub-Resource Locator Pattern

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.


Part 5.2 — HTTP 422 vs 404 for Missing References

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.


Part 5.4 — Security Risks of Exposing Stack Traces

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.


Part 5.5 — JAX-RS Filters for Cross-Cutting Concerns

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)

About

Smart Campus Sensor & Room Management API - 5COSC022W

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages