Skip to content

Thanukamax/smart-campus-api

Repository files navigation

smart-campus-api

Smart Campus Sensor & Room Management API — JAX-RS RESTful service for managing campus rooms, sensors, and sensor readings. University of Westminster CSA Coursework (5COSC022W).

Tech Stack

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)

Project Structure

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

API Endpoints

GET /api/v1 — Discovery

Returns API metadata and available resource links.

Rooms — /api/v1/rooms

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)

Sensors — /api/v1/sensors

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

Setup

# Clone and setup (configures git hooks, compiles project)
git clone https://github.com/Thanukamax/smart-campus-api.git
cd smart-campus-api
./scripts/setup.sh

Or manually:

git config core.hooksPath .githooks
./mvnw clean compile

Run

# Starts on http://localhost:8080
./mvnw exec:java

Code Quality

# Format code (like Prettier)
./mvnw spotless:apply

# Check formatting
./mvnw spotless:check

# Run Checkstyle (like ESLint)
./mvnw checkstyle:check

These run automatically via git hooks on every commit.

Quick Test (Sample curl Commands)

# 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"}' | jq

Conceptual Report

Part 1: Service Architecture & Setup

Q: 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.


Part 2: Room Management

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:

  1. First call (room exists, no sensors): Room is deleted → 204 No Content.
  2. Second call (room no longer exists): Returns 204 No Content again (no error).
  3. 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).


Part 3: Sensor Operations & Linking

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:

  1. Semantics: In REST, the path identifies a resource. /sensors/type/CO2 implies "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.

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

  3. Optionality: Query parameters are inherently optional — GET /sensors returns all, GET /sensors?type=CO2 returns filtered. With path-based approaches, you need separate routes for filtered vs. unfiltered.

  4. Cacheability: Proxies and CDNs handle query parameters as cache keys by default. Changing the path structure can break caching strategies.


Part 4: Deep Nesting with Sub-Resources

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:

  1. Separation of concerns: Each resource class has a single responsibility. SensorResource manages sensors; SensorReadingResource manages readings. Neither class is bloated with methods for the other's domain.

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

  3. Reusability: The SensorReadingResource class could potentially be reused or extended for different parent contexts without duplicating logic.

  4. Contextual construction: The locator method passes the sensorId to 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.


Part 5: Advanced Error Handling, Exception Mapping & Logging

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:

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

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

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

  4. Non-invasive: Filters are registered via @Provider and picked up automatically by the JAX-RS runtime. Adding or removing logging requires zero changes to resource classes.

  5. Ordered pipeline: Multiple filters can be chained and ordered with @Priority, creating a composable processing pipeline (e.g., logging → authentication → rate limiting → resource).


License

MIT

About

Smart Campus Sensor & Room Management API — JAX-RS RESTful service (University of Westminster CSA Coursework)

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors