Skip to content

nickmonick/SmartCampusAPI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Nicholas Monickoraja - w2087567 5COSC022W Client-Server Architectures Coursework Video: https://drive.google.com/file/d/1NNUox4VkAqTbXCzXuUPMVLWVztsOfONu/view?usp=sharing

Smart Campus Sensor and Room Management API

Overview

This is a RESTful API for managing campus rooms and IoT sensors. It is built using JAX-RS (Jersey 2.32) deployed on Apache Tomcat 9.x. Data is stored entirely in memory using HashMaps and ArrayLists with no database. The API supports creating and querying rooms and sensors, recording sensor readings, and handles errors through custom exception mappers.

Base URL: http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1

Build and Run Instructions

Prerequisites: Java 8+, Apache Maven 3.x, Apache Tomcat 9.x

  1. Build the WAR file: mvn clean package

  2. Copy the WAR to Tomcat: cp target/SmartCampusAPI-1.0-SNAPSHOT.war $CATALINA_HOME/webapps/

  3. Start Tomcat: $CATALINA_HOME/bin/startup.sh

  4. Verify it is running: curl http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1

Endpoints

GET /api/v1 Discovery endpoint GET /api/v1/rooms List all rooms POST /api/v1/rooms Create a room (201) GET /api/v1/rooms/{id} Get room by ID (200 / 404) DELETE /api/v1/rooms/{id} Delete room (204 / 409 if sensors assigned) GET /api/v1/sensors List all sensors (optional ?type= filter) POST /api/v1/sensors Create sensor (201 / 422 if roomId invalid) GET /api/v1/sensors/{id} Get sensor by ID (200 / 404) GET /api/v1/sensors/{id}/readings Get reading history POST /api/v1/sensors/{id}/readings Add a reading (201 / 403 if MAINTENANCE)

Sample curl Commands

# 1. Discovery endpoint
curl http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1

# 2. List all rooms
curl http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/rooms

# 3. Create a new room
curl -X POST http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/rooms \
  -H "Content-Type: application/json" \
  -d '{"id":"HALL-001","name":"Main Hall","capacity":200}'

# 4. Create a sensor (links to LAB-101)
curl -X POST http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/sensors \
  -H "Content-Type: application/json" \
  -d '{"id":"HUM-001","type":"Humidity","status":"ACTIVE","currentValue":60.0,"roomId":"LAB-101"}'

# 5. Filter sensors by type
curl "http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/sensors?type=CO2"

# 6. Post a reading to TEMP-001
curl -X POST http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/sensors/TEMP-001/readings \
  -H "Content-Type: application/json" \
  -d '{"value":24.8}'

# 7. Get reading history for TEMP-001
curl http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/sensors/TEMP-001/readings

# 8. Attempt to delete a room that still has sensors (expect 409)
curl -X DELETE http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/rooms/LIB-301

# 9. Post a reading to a MAINTENANCE sensor (expect 403)
curl -X POST http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/sensors/OCC-001/readings \
  -H "Content-Type: application/json" \
  -d '{"value":15.0}'

# 10. Create a sensor with a non-existent roomId (expect 422)
curl -X POST http://localhost:8080/SmartCampusAPI-1.0-SNAPSHOT/api/v1/sensors \
  -H "Content-Type: application/json" \
  -d '{"id":"BAD-001","type":"CO2","status":"ACTIVE","currentValue":0.0,"roomId":"NONEXISTENT"}'

Pre-seeded Data

Rooms: LIB-301 (Library Quiet Study, capacity 50), LAB-101 (Computer Lab 1, capacity 30) Sensors: TEMP-001 (Temperature, ACTIVE, LIB-301), CO2-001 (CO2, ACTIVE, LAB-101), OCC-001 (Occupancy, MAINTENANCE, LIB-301)


Report

Part 1 - Setup and Architecture

Q1.1 - JAX-RS Resource Lifecycle and Thread Safety

By default, JAX-RS creates a new instance of each resource class for every incoming HTTP request. This means instance variables inside a resource class are not shared between concurrent requests, so they are thread-safe by isolation. Each request gets its own object.

However, this project uses a singleton DataStore (DataStore.getInstance()) which is shared across all requests. When two requests hit the server at the same time, for example two POST /sensors calls, both access and modify the same HashMap. This creates a race condition where two threads might both read the map, decide a key does not exist, and both insert, resulting in data corruption.

To prevent this you could use ConcurrentHashMap instead of HashMap, use synchronized blocks around critical sections, or use Collections.synchronizedMap(). For this coursework the spec states concurrency is not required, so a plain HashMap is used and thread-safety is not a concern in the single-user demo context.

If a resource class is annotated with @Singleton, JAX-RS creates only one instance for the whole application lifetime. This is more efficient but requires all fields to be thread-safe since all threads share one object.

Q1.2 - HATEOAS and Hypermedia Links

HATEOAS stands for Hypermedia As The Engine Of Application State. It is the highest maturity level of RESTful API design in the Richardson Maturity Model. The idea is that API responses include navigational links that tell the client what actions are available next, rather than the client having to hardcode URLs.

Without HATEOAS a client has to know from external documentation that it must call /api/v1/rooms/LIB-301 to get a room's details. If the URL structure changes, clients break. With HATEOAS a response would include a _links field with a self link, a sensors link, and a delete link, so the client just follows what the server provides.

This benefits client developers in several ways. First, discoverability: clients can explore the API without reading external docs. Second, decoupling: if the server changes a URL, clients following links rather than hardcoded strings adapt automatically. Third, it reduces client complexity because the server drives the workflow. The GET /api/v1 discovery endpoint in this project is a practical example as it returns links to all primary resource collections.

Part 2 - Room Management

Q2.1 - Returning IDs vs Full Objects in Collections

Returning only IDs produces a very small payload and minimises bandwidth, but the client then has to make one additional GET request for every room to get its details. With 500 rooms that means 500 extra HTTP round-trips, which is called the N+1 problem.

Returning full objects means all data arrives in one request with no additional round-trips, which is better for a dashboard that needs to display full room details immediately. The downside is a larger payload, which can be expensive on mobile networks, and the client receives fields it may not need.

REST APIs should be designed around client needs. Twitter's API v2 uses sparse fieldsets so clients request only the fields they need. For this coursework, returning full objects is appropriate given the small data set and the admin dashboard use case where complete details are always needed.

Q2.2 - DELETE Idempotency

An operation is idempotent if applying it once produces the same server state as applying it multiple times.

The first DELETE /api/v1/rooms/HALL-001 finds the room, removes it from the DataStore, and returns 204 No Content. A second DELETE on the same room finds nothing and returns 404 Not Found. The server state is the same after both calls: the room does not exist. According to RFC 7231, DELETE is idempotent because the end state is identical regardless of how many times it is called. The different response codes just communicate the state of the resource at the time of the request, not a change in server state.

This is different from POST which is not idempotent. Sending POST /api/v1/rooms twice with the same body would attempt to create two rooms, resulting in different server state each time.

Part 3 - Sensors and Filtering

Q3.1 - @Consumes and Content-Type Mismatch

The @Consumes(MediaType.APPLICATION_JSON) annotation tells the JAX-RS runtime that a method can only process requests whose Content-Type header is application/json. This is checked before the method is invoked.

If a client sends Content-Type: text/plain, the JAX-RS runtime inspects the incoming header, searches all registered methods for one that matches, finds no match, and immediately returns HTTP 415 Unsupported Media Type without calling any Java code.

This protects the server from accidentally trying to deserialise malformed data. Without @Consumes, a malicious client could send arbitrary bytes that cause the JSON parser to crash and trigger a 500 error, which might leak stack trace information about the server's internals.

Q3.2 - @QueryParam vs Path Parameters for Filtering

A path-based approach like /api/v1/sensors/type/CO2 makes the type part of the resource identity, implying it is a distinct resource rather than a filtered view. It also becomes awkward with multiple filters, for example /api/v1/sensors/type/CO2/status/ACTIVE/room/LIB-301, and can conflict with existing path parameters like {sensorId}.

The query parameter approach keeps the base resource consistent: /api/v1/sensors always means all sensors, and ?type=CO2 is just a modifier. Multiple filters compose naturally: ?type=CO2&status=ACTIVE&roomId=LIB-301. Parameters can also be omitted without changing the URI structure.

The general REST design principle is that path parameters identify a specific resource (e.g., /sensors/TEMP-001) while query parameters filter or configure a collection. This is the standard convention used by GitHub, Twitter, and Stripe, among others.

Part 4 - Sub-Resources

Q4.1 - Sub-Resource Locator Pattern

A sub-resource locator is a JAX-RS method annotated with @Path but without an HTTP method annotation like @GET or @POST. Instead of handling a request directly, it returns an object whose methods handle the request. JAX-RS then inspects that returned object for annotated methods.

In this project, SensorResource has a method annotated with @Path("/{sensorId}/readings") that returns a new SensorReadingResource instance. The sensor ID is validated before the delegate is returned, so the reading resource does not need to re-check it.

The main benefits are separation of concerns (SensorResource handles sensor logic and SensorReadingResource handles reading logic, each with a single focused responsibility), complexity management (in a large API, putting every endpoint in one class creates a god class that is impossible to maintain), and testability (each class can be unit tested independently). In a team, different developers can also work on each class simultaneously without merge conflicts.

Part 5 - Error Handling

Q5.2 - Why HTTP 422 over HTTP 404

HTTP 404 means the requested URI does not exist. It answers the question of whether the endpoint is valid. HTTP 422 Unprocessable Entity means the server understood the request format but cannot process it because of a semantic error inside the payload.

When POST /api/v1/sensors is sent with "roomId": "ROOM-999", the URI /api/v1/sensors does exist, so 404 is wrong. The JSON is valid, so 400 Bad Request (which implies malformed syntax) is also not ideal. The problem is that the payload references a room that does not exist, which is a semantic rather than a syntactic error. 422 correctly communicates that the request was understood but cannot be fulfilled because a referenced dependency is missing.

HTTP 400 is acceptable per the spec but 422 is more precise. 400 means the request is malformed, while 422 means it is syntactically correct but logically invalid.

Q5.4 - Cybersecurity Risk of Exposing Stack Traces

A stack trace reveals several things an attacker can use. It exposes internal class structure such as package names (e.g. com.smartcampus.data.DataStore) which reveals the software architecture. It reveals framework versions (e.g. jersey-server-2.32) that can be cross-referenced against known CVEs. Errors from database layers often include table names, column names, and SQL fragments. Exception messages can reveal which code paths exist and how to trigger them. Stack traces may also include the server operating system, JVM version, and deployment paths.

The principle of least information says clients only need to know that an error occurred and what they should do next. They should never learn how the server works internally. The GenericExceptionMapper in this project solves this by logging the full stack trace server-side for administrators while returning only a safe generic JSON message to the client.

Q5.5 - JAX-RS Filters vs Manual Logging

Logging is a cross-cutting concern because it applies to every request, not to any single resource. If logging were added manually to every resource method, you would have identical Logger.info() code duplicated across every class, which violates the DRY principle. If a developer forgets to add it to a new method, that method is invisible in logs. Changing the log format would require editing every resource method. Business logic in resource classes also becomes harder to read when mixed with infrastructure concerns.

Using a ContainerRequestFilter and ContainerResponseFilter means one LoggingFilter class intercepts all requests and responses automatically. No request can bypass it, newly added resource methods are automatically logged, and changing the log format means editing one class. Resource classes stay clean and focused on business logic. This is equivalent to the Decorator or Chain of Responsibility pattern, and the same approach would be used for authentication, compression, or CORS in a production API.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages