diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..58f8b5c --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,13 @@ +#!/bin/sh +echo '=====================================================' +echo 'Running pre-commit checks (Spotless, Checkstyle)...' +echo '=====================================================' +if ! mvn spotless:check checkstyle:check; then + echo '=====================================================' + echo '❌ PRE-COMMIT FAILED: Code quality checks did not pass.' + echo 'Please fix the errors above or run "mvn spotless:apply" to format.' + echo '=====================================================' + exit 1 +fi +echo '✅ All checks passed! Proceeding with commit.' +exit 0 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..5df3235 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,82 @@ +name: Manual Deployment + +on: + workflow_dispatch: # Allows manual triggering from GitHub UI + +jobs: + # Job 1: Tooling System & Testing + validate-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Validate & Test (Format, Lint, Test) + run: mvn spotless:check checkstyle:check test + + # Job 2: Build the Deployment Artifact + build-artifact: + needs: validate-and-test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + - name: Build Fat JAR + run: mvn clean package -DskipTests + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: deployment-jar + path: target/basic-web-server-1.0-SNAPSHOT.jar + + # Job 3: Actual Deployment to Target Server + deploy: + needs: build-artifact + runs-on: ubuntu-latest + steps: + - name: Download Artifact + uses: actions/download-artifact@v4 + with: + name: deployment-jar + + - name: Copy Artifact to VM via SCP + uses: appleboy/scp-action@v1 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + port: ${{ secrets.SERVER_PORT || '22' }} + source: "basic-web-server-1.0-SNAPSHOT.jar" + target: "/opt/basic-web-server/" + + - name: Restart Service via SSH + uses: appleboy/ssh-action@v1 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USER }} + key: ${{ secrets.SERVER_SSH_KEY }} + port: ${{ secrets.SERVER_PORT || '22' }} + script: | + echo "Deployment starting on remote VM..." + cd /opt/basic-web-server + + # If you are using a systemd service (recommended professional approach): + # sudo systemctl restart basic-web-server + + # Or if you are using a raw shell script to restart: + echo "Stopping old server instance..." + pkill -f 'basic-web-server-1.0-SNAPSHOT.jar' || true + + echo "Starting new server instance in background..." + nohup java -jar basic-web-server-1.0-SNAPSHOT.jar > server.log 2>&1 & + + echo "Deployment completed successfully!" diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..0d00897 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,26 @@ +name: PR Check + +on: + pull_request: + branches: [ main ] + +jobs: + validate-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - name: Set up JDK 17 + uses: actions/setup-java@v6 + with: + java-version: '17' + distribution: 'temurin' + cache: maven + + - name: Formatter Check (Spotless) + run: mvn spotless:check + + - name: Linter Check (Checkstyle) + run: mvn checkstyle:check + + - name: Type Check & Test (Compile + JUnit) + run: mvn clean test diff --git a/.gitignore b/.gitignore index b497ff1..c53af64 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,46 @@ -bin/ -.vscode/ \ No newline at end of file +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +# IDEs +.idea/ +*.iml +*.iws +.classpath +.project +.settings/ +.vscode/ +bin/ \ No newline at end of file diff --git a/README.md b/README.md index f0c5cab..1598104 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,22 @@ -# Basic Java Web Server Project +# Basic Java Web Server REST API -This is a basic web server project built with **native Java**, utilizing only the Java standard library without any external frameworks. It demonstrates fundamental concepts such as routing, handling different HTTP methods, and processing URL parameters and query strings. The goal of this project is to serve as a reference for anyone interested in understanding how a web service can be implemented using just Java. +This is a basic web server project originally built with native Java, now refactored into a **professional RESTful JSON API**. It demonstrates fundamental backend concepts such as handling HTTP methods, thread pools for concurrency, routing, and processing JSON payloads. The goal of this project is to serve as an educational reference for building a modern web service in Java from scratch. ## Features -- **Basic Routing**: Set up routes for different endpoints. -- **Route with Parameters**: Handle dynamic routes with URL parameters. -- **Query Parameters**: Extract and process query strings from the URL. -- **HTTP Methods**: Support for common HTTP methods: `GET`, `POST`, `PATCH`, `DELETE`. +- **REST API (JSON)**: Fully responds and consumes `application/json` using Google's Gson library. +- **Concurrency**: Built-in `ThreadPoolExecutor` for handling multiple HTTP requests simultaneously. +- **Maven Architecture**: Standardized project structure and dependency management. +- **Dev Tooling**: Code automatically formatted via **Spotless** (Google Java Format) and linted via **Checkstyle**. +- **CI/CD Ready**: Includes GitHub Actions for PR checks (Format/Lint/Test) and multi-stage manual deployment pipelines using SCP/SSH. ## Getting Started ### Prerequisites -Ensure you have **Java Development Kit (JDK)** installed on your machine. - -- [Download JDK](https://www.oracle.com/java/technologies/downloads/) (or the version you prefer) +Ensure you have **Java Development Kit (JDK) 17+** and **Apache Maven** installed on your machine. ### Installation @@ -32,64 +31,145 @@ Ensure you have **Java Development Kit (JDK)** installed on your machine. ### Running the Server -1. Compile Java program first: - ```bash - javac -d bin src/Main.java src/controller/*.java src/model/*.java src/utils/*.java - ``` -2. Run compiled program - ```bash - java -cp bin Main - ``` +Using Maven, you can compile and start the server immediately: +```bash +mvn clean compile exec:java -Dexec.mainClass="com.server.Main" +``` +The server will be running at `http://localhost:8000` -The server will be running at http://localhost:8000 +### Building the Executable JAR +If you want to build a deployment-ready Fat JAR: +```bash +mvn clean package -DskipTests +java -jar target/basic-web-server-1.0-SNAPSHOT.jar +``` +## Tooling & Testing -## Endpoints -Here are the key endpoints that you can interact with: +Run the automated tests: +```bash +mvn test +``` +Format the codebase and check for linting errors: +```bash +mvn spotless:apply checkstyle:check ``` -GET / : Returns a simple welcome message and provide simple form to handle request input -GET /users : Fetch all users -GET /users/:id : Fetch a data by their ID -POST /users : Add a new user -PATCH /users/:id : Update user information -DELETE /users/:id : Delete a user by their ID -GET /search?name=&job= : Retrieves data based on user input from a search form, filtering results by name and job + +### Pre-commit Hooks +This project strictly enforces code quality using Git pre-commit hooks (Spotless and Checkstyle). +After cloning the repository, you **must** configure Git to use the local hooks directory: +```bash +git config core.hooksPath .githooks ``` +Every time you commit, it will automatically verify formatting and linting. If it fails, the commit will be aborted. -## Query Parameters -Example: /search?name=John&job=data+analyst -Query parameters can be extracted and used in request processing. +## Endpoints +Here are the key REST endpoints you can interact with. All endpoints strictly consume and return `application/json`. + +### 1. API Health Check +- **`GET /`** + - **Response (200 OK):** + ```json + { + "status": "UP", + "message": "Welcome to Basic Web Server API", + "totalUsers": 8 + } + ``` -## HTTP Methods -The service supports the following HTTP methods: +### 2. Fetch All Users +- **`GET /users`** + - **Response (200 OK):** + ```json + [ + { "name": "Alex", "job": "Data Analyst" }, + { "name": "Budi", "job": "Frontend Web Developer" } + ] + ``` -``` -GET: Retrieve data. -POST: Submit new data. -PATCH: Update partial existing data. -DELETE: Remove data. -``` +### 3. Fetch User by ID +- **`GET /users/:id`** *(e.g. `/users/1`)* + - **Response (200 OK):** + ```json + { + "name": "Alex", + "job": "Data Analyst" + } + ``` + +### 4. Create New User +- **`POST /users`** + - **Request Body:** + ```json + { + "name": "John Doe", + "job": "Backend Engineer" + } + ``` + - **Response (201 Created):** + ```json + { + "status": "success", + "message": "User added successfully" + } + ``` + +### 5. Update User Information +- **`PATCH /users/:id`** *(e.g. `/users/1`)* + - **Request Body:** + ```json + { + "job": "Senior Data Analyst" + } + ``` + - **Response (200 OK):** + ```json + { + "status": "success", + "message": "Data updated successfully" + } + ``` + +### 6. Delete a User +- **`DELETE /users/:id`** *(e.g. `/users/1`)* + - **Response (200 OK):** + ```json + { + "status": "success", + "message": "Data deleted successfully" + } + ``` + +### 7. Search Users +- **`GET /search?name=&job=`** *(e.g. `/search?job=Data Analyst`)* + - **Response (200 OK):** + ```json + [ + { "name": "Alex", "job": "Data Analyst" }, + { "name": "Dono", "job": "Data Analyst" } + ] + ``` ## Code Structure -The project is simple and has the following structure: +The project uses the standard Maven structure: ```bash /root -├───bin # compiled Java file -│ ├─── /controller -│ ├─── /model -│ └─── /utils -├───lib +├───.github/workflows # CI/CD Pipelines +├───pom.xml # Build config and dependencies └───src - ├─── /controller # A handler for any request - ├─── /model # A temporary data user blueprint - ├─── /utils # Some helper function - └─── Main.java # Main server logic + ├───main/java/com/server + │ ├─── /controller # HTTP route handlers + │ ├─── /model # Data structures and tracking + │ ├─── /utils # Parsers and HTTP helpers + │ └─── Main.java # Server initialization and Thread Pool + └───test/java/com/server + └─── /model # JUnit 5 test cases ``` ## License This project is licensed under the MIT License. ## Acknowledgments -This project is intended to help others learn the basics of building a web service with Java native ☕. \ No newline at end of file +This project is intended to help others learn the basics of building a robust REST API with Java ☕. \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a48dc8f --- /dev/null +++ b/pom.xml @@ -0,0 +1,81 @@ + + + 4.0.0 + + com.server + basic-web-server + 1.0-SNAPSHOT + + + 17 + 17 + + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + + com.google.code.gson + gson + 2.10.1 + + + + + + + + com.diffplug.spotless + spotless-maven-plugin + 2.40.0 + + + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + 3.3.1 + + google_checks.xml + true + true + false + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + shade + + + + com.server.Main + + + + + + + + + diff --git a/src/Main.java b/src/Main.java deleted file mode 100644 index d2719a9..0000000 --- a/src/Main.java +++ /dev/null @@ -1,63 +0,0 @@ -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.ServerSocket; -import java.net.Socket; - -import controller.RootController; -import controller.UsersController; -import model.Users; -import utils.HttpUtils; - -public class Main { - private static Users data = new Users(); - private static RootController rootController = new RootController(data); - private static UsersController usersController = new UsersController(data); - - public static void main(String[] args) throws Exception { - try (ServerSocket serverSocket = new ServerSocket(8000)) { - System.out.println("Server is running on http://localhost:" + serverSocket.getLocalPort()); - - while (true) { - try (Socket clientSocket = serverSocket.accept()) { - handleRequest(clientSocket); - } catch (Exception e) { - e.printStackTrace(); - } - } - } catch (IOException e) { - e.printStackTrace(); - } - } - - private static void handleRequest(Socket clientSocket) { - try (BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) { - String requestLine = in.readLine(); - - // client HTTP request line log - System.out.println(requestLine); - - // Routing - if (requestLine.startsWith("GET / ")) { - rootController.handleGetRequest(clientSocket); - } else if (requestLine.startsWith("GET /users ")) { - usersController.getAllUsers(clientSocket); - } else if (requestLine.startsWith("POST /users ")) { - usersController.postUserData(clientSocket, in); - } else if (requestLine.startsWith("GET /users/")) { - usersController.getUserDataById(clientSocket, requestLine); - } else if (requestLine.startsWith("PATCH /users/")) { - usersController.updateUserDataById(clientSocket, in, requestLine); - } else if (requestLine.startsWith("DELETE /users/")) { - usersController.deleteUserDataById(clientSocket, requestLine); - } else if (requestLine.startsWith("GET /search?")) { - usersController.getUserDataByQuery(clientSocket, requestLine); - } else { - HttpUtils.handle404ErrorResponse(clientSocket); - } - } catch (Exception e) { - HttpUtils.handleServerErrorResponse(clientSocket, e); - } - } - -} diff --git a/src/controller/RootController.java b/src/controller/RootController.java deleted file mode 100644 index 048c4e4..0000000 --- a/src/controller/RootController.java +++ /dev/null @@ -1,68 +0,0 @@ -package controller; - -import java.io.IOException; -import java.io.PrintWriter; -import java.net.Socket; - -import model.Users; - -public class RootController { - private Users data; - - public RootController(Users data) { - this.data = data; - } - - public void handleGetRequest(Socket clientSocket) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); - out.println(); - - // HTTP response body - out.println( - """ - - - - Root Page - - - -

Welcome! You successfully get response from server

-

Current user: """+data.getArrayLength()+""" -

- -
- -
-

Send user data to server!

-
- -
- -
- -
-
- -
-

Search user data!

-
- -
- -
- -
-

*) Empty search box will get all user data

-
- - - - """); - } - } -} \ No newline at end of file diff --git a/src/controller/UsersController.java b/src/controller/UsersController.java deleted file mode 100644 index 7cda643..0000000 --- a/src/controller/UsersController.java +++ /dev/null @@ -1,313 +0,0 @@ -package controller; - -import java.io.BufferedReader; -import java.io.PrintWriter; -import java.io.IOException; -import java.util.ArrayList; -import java.util.Map; -import java.net.Socket; -import java.net.URI; -import java.net.URISyntaxException; - -import utils.HttpUtils; -import utils.UserUtils; -import model.User; -import model.Users; - -public class UsersController { - private Users data; - - public UsersController(Users data) { - this.data = data; - } - - public void getAllUsers(Socket clientSocket) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - ArrayList dataResult = data.getAllData(); - String result = "
    "; - for (User datum : dataResult) { - result += """ -
  1. -

    Name : """+datum.getName()+""" -

    -

    Job : """+datum.getJob()+""" -

    -
  2. - """; - } - result += "
"; - - - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); - out.println(); - - // HTTP response body - out.println( - """ - - - Welcome! - - -

This is all users data

- """+result+""" - - - """); - } - } - - public void getUserDataById(Socket clientSocket, String requestLine) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - int userId = UserUtils.getUserId(requestLine); - - String result = null; - if (userId != -1 && userId <= data.getArrayLength()) { - result = """ -

You get data from the server

-

ID: """+userId+""" -

-

Data: -

-

- -

-
-
-

Update this user data!

-
- -
- -
- -
-
- -

- -
-

Delete this user data!

-
- -
-
-
- - - """; - } - - if (result != null) { - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); - out.println(); - - // HTTP response body - out.println( - """ - - - User Data - - - """+ result +""" - - - """); - } else { - HttpUtils.handle404ErrorResponse(clientSocket); - } - } - } - - public void getUserDataByQuery(Socket clientSocket, String requestLine) throws IOException, URISyntaxException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - String[] arrayRequestLine = requestLine.split(" "); - URI uri = new URI(arrayRequestLine[1]); - - Map queryParams = HttpUtils.extractQueryParams(uri.getQuery()); - ArrayList dataResult = data.getUserDataByQuery(queryParams); - - String dataHTMLContent; - if (!dataResult.isEmpty()) { - dataHTMLContent = "
    "; - for (User datum : dataResult) { - dataHTMLContent += """ -
  • -

    Name: """+datum.getName()+""" -

    -

    Job: """+datum.getJob()+""" -

    -
  • - """; - } - dataHTMLContent += "
"; - } else { - dataHTMLContent = "

Data not found!

"; - } - - - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); - out.println(); - - // HTTP response body - out.println( - """ -

Search result!

- """+dataHTMLContent); - } - } - - public void postUserData(Socket clientSocket, BufferedReader in) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - String requestBody = HttpUtils.getRequestBody(in); - String newName = HttpUtils.extractQueryParams(requestBody).get("name"); - String newJob = HttpUtils.extractQueryParams(requestBody).get("job"); - - data.addNewData(new User(newName, newJob)); - - - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); - out.println(); - - // HTTP response body - out.println( - """ -

Data successfully submitted to server

- - - - """); - } - } - - public void updateUserDataById(Socket clientSocket, BufferedReader in, String requestLine) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - String requestBody = HttpUtils.getRequestBody(in); - int userId = UserUtils.getUserId(requestLine); - - String newName = HttpUtils.extractQueryParams(requestBody).get("new-name"); - String newJob = HttpUtils.extractQueryParams(requestBody).get("new-job"); - - if (!newName.equals("") && newName != null) { - data.getUserDataByIndex(userId - 1).setName(newName); - } - - if (!newJob.equals("") && newJob != null) { - data.getUserDataByIndex(userId - 1).setJob(newJob); - } - - - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: application/json"); - out.println(); - - // HTTP response body - out.println( - """ - { - "status": "success", - "message": "Data updated successfully" - } - """); - } - } - - public void deleteUserDataById(Socket clientSocket, String requestLine) throws IOException { - System.out.println("delete bang!"); - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - - int userId = UserUtils.getUserId(requestLine); - - data.removeData(userId - 1); - - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: application/json"); - out.println(); - - // HTTP response body - out.println( - """ - { - "status": "success", - "message": "Data deleted successfully" - } - """); - } - } -} \ No newline at end of file diff --git a/src/main/java/com/server/Main.java b/src/main/java/com/server/Main.java new file mode 100644 index 0000000..616a950 --- /dev/null +++ b/src/main/java/com/server/Main.java @@ -0,0 +1,70 @@ +package com.server; + +import com.server.controller.RootController; +import com.server.controller.UsersController; +import com.server.model.Users; +import com.server.utils.HttpUtils; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +public class Main { + private static Users data = new Users(); + private static RootController rootController = new RootController(data); + private static UsersController usersController = new UsersController(data); + private static final ExecutorService threadPool = Executors.newFixedThreadPool(10); + + public static void main(String[] args) throws Exception { + try (ServerSocket serverSocket = new ServerSocket(8000)) { + System.out.println("Server is running on http://localhost:" + serverSocket.getLocalPort()); + + while (true) { + try { + Socket clientSocket = serverSocket.accept(); + threadPool.submit(() -> handleRequest(clientSocket)); + } catch (Exception e) { + e.printStackTrace(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static void handleRequest(Socket clientSocket) { + try (clientSocket; + BufferedReader in = + new BufferedReader(new InputStreamReader(clientSocket.getInputStream()))) { + String requestLine = in.readLine(); + + // client HTTP request line log + if (requestLine == null) return; + System.out.println(requestLine); + + // Routing + if (requestLine.startsWith("GET / ")) { + rootController.handleGetRequest(clientSocket); + } else if (requestLine.startsWith("GET /users ")) { + usersController.getAllUsers(clientSocket); + } else if (requestLine.startsWith("POST /users ")) { + usersController.postUserData(clientSocket, in); + } else if (requestLine.startsWith("GET /users/")) { + usersController.getUserDataById(clientSocket, requestLine); + } else if (requestLine.startsWith("PATCH /users/")) { + usersController.updateUserDataById(clientSocket, in, requestLine); + } else if (requestLine.startsWith("DELETE /users/")) { + usersController.deleteUserDataById(clientSocket, requestLine); + } else if (requestLine.startsWith("GET /search?")) { + usersController.getUserDataByQuery(clientSocket, requestLine); + } else { + HttpUtils.handle404ErrorResponse(clientSocket); + } + } catch (Exception e) { + HttpUtils.handleServerErrorResponse(clientSocket, e); + } + } +} diff --git a/src/main/java/com/server/controller/RootController.java b/src/main/java/com/server/controller/RootController.java new file mode 100644 index 0000000..adcb3a0 --- /dev/null +++ b/src/main/java/com/server/controller/RootController.java @@ -0,0 +1,48 @@ +package com.server.controller; + +import com.google.gson.Gson; +import com.server.model.Users; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; + +/** Controller for handling requests to the root endpoint. */ +public class RootController { + private Users data; + private Gson gson = new Gson(); + + /** + * Constructs the root controller. + * + * @param data The user data model. + */ + public RootController(Users data) { + this.data = data; + } + + /** + * Handles GET requests to the root endpoint. + * + * @param clientSocket The client socket. + * @throws IOException If an I/O error occurs. + */ + public void handleGetRequest(Socket clientSocket) throws IOException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + + Map responseBody = new HashMap<>(); + responseBody.put("status", "UP"); + responseBody.put("message", "Welcome to Basic Web Server API"); + responseBody.put("totalUsers", data.getArrayLength()); + + // HTTP response header + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: application/json"); + out.println(); + + // HTTP response body + out.println(gson.toJson(responseBody)); + } + } +} diff --git a/src/main/java/com/server/controller/UsersController.java b/src/main/java/com/server/controller/UsersController.java new file mode 100644 index 0000000..39e0b1f --- /dev/null +++ b/src/main/java/com/server/controller/UsersController.java @@ -0,0 +1,143 @@ +package com.server.controller; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import com.server.model.User; +import com.server.model.Users; +import com.server.utils.HttpUtils; +import com.server.utils.UserUtils; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Map; + +/** Controller for user endpoints, refactored to REST API (JSON). */ +public class UsersController { + private Users data; + private Gson gson = new Gson(); + + public UsersController(Users data) { + this.data = data; + } + + public void getAllUsers(Socket clientSocket) throws IOException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + ArrayList dataResult = data.getAllData(); + + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: application/json"); + out.println(); + out.println(gson.toJson(dataResult)); + } + } + + public void getUserDataById(Socket clientSocket, String requestLine) throws IOException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + int userId = UserUtils.getUserId(requestLine); + + if (userId != -1 && userId <= data.getArrayLength()) { + User user = data.getUserDataByIndex(userId - 1); + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: application/json"); + out.println(); + out.println(gson.toJson(user)); + } else { + HttpUtils.handle404ErrorResponse(clientSocket); + } + } + } + + public void getUserDataByQuery(Socket clientSocket, String requestLine) + throws IOException, URISyntaxException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + + String[] arrayRequestLine = requestLine.split(" "); + URI uri = new URI(arrayRequestLine[1]); + + Map queryParams = HttpUtils.extractQueryParams(uri.getQuery()); + ArrayList dataResult = data.getUserDataByQuery(queryParams); + + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: application/json"); + out.println(); + out.println(gson.toJson(dataResult)); + } + } + + public void postUserData(Socket clientSocket, BufferedReader in) throws IOException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + String requestBody = HttpUtils.getRequestBody(in); + + User newUser = gson.fromJson(requestBody, User.class); + data.addNewData(newUser); + + JsonObject response = new JsonObject(); + response.addProperty("status", "success"); + response.addProperty("message", "User added successfully"); + + out.println("HTTP/1.1 201 Created"); + out.println("Content-Type: application/json"); + out.println(); + out.println(gson.toJson(response)); + } + } + + public void updateUserDataById(Socket clientSocket, BufferedReader in, String requestLine) + throws IOException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + String requestBody = HttpUtils.getRequestBody(in); + int userId = UserUtils.getUserId(requestLine); + + if (userId == -1 || userId > data.getArrayLength()) { + HttpUtils.handle404ErrorResponse(clientSocket); + return; + } + + User updates = gson.fromJson(requestBody, User.class); + User existingUser = data.getUserDataByIndex(userId - 1); + + if (updates.getName() != null && !updates.getName().trim().isEmpty()) { + existingUser.setName(updates.getName()); + } + if (updates.getJob() != null && !updates.getJob().trim().isEmpty()) { + existingUser.setJob(updates.getJob()); + } + + JsonObject response = new JsonObject(); + response.addProperty("status", "success"); + response.addProperty("message", "Data updated successfully"); + + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: application/json"); + out.println(); + out.println(gson.toJson(response)); + } + } + + public void deleteUserDataById(Socket clientSocket, String requestLine) throws IOException { + System.out.println("delete request received!"); + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + int userId = UserUtils.getUserId(requestLine); + + if (userId == -1 || userId > data.getArrayLength()) { + HttpUtils.handle404ErrorResponse(clientSocket); + return; + } + + data.removeData(userId - 1); + + JsonObject response = new JsonObject(); + response.addProperty("status", "success"); + response.addProperty("message", "Data deleted successfully"); + + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: application/json"); + out.println(); + out.println(gson.toJson(response)); + } + } +} diff --git a/src/main/java/com/server/model/User.java b/src/main/java/com/server/model/User.java new file mode 100644 index 0000000..017aa0c --- /dev/null +++ b/src/main/java/com/server/model/User.java @@ -0,0 +1,26 @@ +package com.server.model; + +public class User { + private String name, job; + + public User(String name, String job) { + this.name = name; + this.job = job; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getJob() { + return job; + } + + public void setJob(String job) { + this.job = job; + } +} diff --git a/src/main/java/com/server/model/Users.java b/src/main/java/com/server/model/Users.java new file mode 100644 index 0000000..799e2a5 --- /dev/null +++ b/src/main/java/com/server/model/Users.java @@ -0,0 +1,73 @@ +package com.server.model; + +import java.util.ArrayList; +import java.util.Map; + +public class Users { + private ArrayList data; + + public Users() { + data = new ArrayList<>(); + + data.add(new User("Alex", "Data Analyst")); + data.add(new User("Budi", "Frontend Web Developer")); + data.add(new User("Cahya", "Computer Vision Engineer")); + data.add(new User("Dono", "Data Analyst")); + data.add(new User("Cakra", "Frontend Web Developer")); + data.add(new User("Zara", "Frontend Web Developer")); + data.add(new User("Jojo", "Machine Learning Engineer")); + data.add(new User("Pepe", "Data Analyst")); + } + + public ArrayList getAllData() { + return data; + } + + public int getArrayLength() { + return data.size(); + } + + public User getUserDataByIndex(int index) { + return data.get(index); + } + + public ArrayList getUserDataByQuery(Map queryParams) { + String nameWanted = queryParams.get("name"); + String jobWanted = queryParams.get("job"); + + ArrayList result = new ArrayList<>(); + + if (!nameWanted.equals("") && !jobWanted.equals("")) { + for (User datum : data) { + if (datum.getName().equalsIgnoreCase(nameWanted) + && datum.getJob().equalsIgnoreCase(jobWanted)) { + result.add(datum); + } + } + } else if (!nameWanted.equals("")) { + for (User datum : data) { + if (datum.getName().equalsIgnoreCase(nameWanted)) { + result.add(datum); + } + } + } else if (!jobWanted.equals("")) { + for (User datum : data) { + if (datum.getJob().equalsIgnoreCase(jobWanted)) { + result.add(datum); + } + } + } else { + result = getAllData(); + } + + return result; + } + + public void addNewData(User newData) { + data.add(newData); + } + + public void removeData(int index) { + data.remove(index); + } +} diff --git a/src/main/java/com/server/utils/HttpUtils.java b/src/main/java/com/server/utils/HttpUtils.java new file mode 100644 index 0000000..57ff863 --- /dev/null +++ b/src/main/java/com/server/utils/HttpUtils.java @@ -0,0 +1,106 @@ +package com.server.utils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.PrintWriter; +import java.net.Socket; +import java.util.HashMap; +import java.util.Map; + +public class HttpUtils { + + /** + * Sends a 404 Not Found response to the client. + * + * @param clientSocket the socket connection to the client + * @throws IOException if an I/O error occurs while sending the response + */ + public static void handle404ErrorResponse(Socket clientSocket) throws IOException { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + out.println("HTTP/1.1 404 Not Found"); + out.println("Content-Type: application/json"); + out.println(); + out.println("{\"error\": \"404 Not Found\"}"); + } + } + + /** + * Sends a 500 Internal Server Error response to the client. + * + * @param clientSocket the socket connection to the client + * @param e the exception that occurred, which will be included in the response message + */ + public static void handleServerErrorResponse(Socket clientSocket, Exception e) { + try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + out.println("HTTP/1.1 500 Internal Server Error"); + out.println("Content-Type: application/json"); + out.println(); + + // We escape double quotes just in case, though for a simple server it is basic + String safeMsg = + e.getMessage() != null ? e.getMessage().replace("\"", "\\\"") : "Unknown Error"; + out.println("{\"error\": \"500 Internal Server Error\", \"details\": \"" + safeMsg + "\"}"); + } catch (IOException ex) { + ex.printStackTrace(); + } + } + + /** + * Retrieves the entire HTTP request body from the given BufferedReader. + * + * @param in the BufferedReader from which the HTTP request body is read + * @return a String containing the entire request body + */ + public static String getRequestBody(BufferedReader in) throws IOException { + String requestBody = null; + int contentLength = getContentLength(in); + + // Read the body content if not empty + if (contentLength > 0) { + char[] body = new char[contentLength]; + try { + in.read(body, 0, contentLength); + } catch (IOException e) { + e.printStackTrace(); + } + requestBody = new String(body); + } + + return requestBody; + } + + private static int getContentLength(BufferedReader in) throws IOException { + String inputLine; + int contentLength = 0; + while ((inputLine = in.readLine()) != null && !inputLine.isEmpty()) { + if (inputLine.startsWith("Content-Length:")) { + contentLength = Integer.parseInt(inputLine.split(":")[1].trim()); + } + } + + return contentLength; + } + + /** + * Extracts raw query parameters from a URL string and returns them as a map. + * + * @param queryParam the URL query parameter string (e.g., "key1=value1&key2=value2") + * @return a Map containing the extracted query parameters, where each key is a parameter name and + * the corresponding value is the parameter value + */ + public static Map extractQueryParams(String queryParam) { + Map result = new HashMap<>(); + String[] pairs = queryParam.split("&"); + + for (String pair : pairs) { + String[] keyValue = pair.split("="); + if (keyValue.length == 2) { + result.put(keyValue[0], keyValue[1]); + } else if (keyValue.length == 1) { + result.put(keyValue[0], ""); + } + } + + return result; + } +} diff --git a/src/main/java/com/server/utils/UserUtils.java b/src/main/java/com/server/utils/UserUtils.java new file mode 100644 index 0000000..eda46ef --- /dev/null +++ b/src/main/java/com/server/utils/UserUtils.java @@ -0,0 +1,23 @@ +package com.server.utils; + +public class UserUtils { + + /** + * Retrieves the user ID from the specified URL request line. + * + * @param requestLine the HTTP request line containing the URL with the user ID parameter + * @return the user ID extracted from the request line + */ + public static int getUserId(String requestLine) { + String[] arrayRequestLine = requestLine.split(" "); + int userId = -1; + + try { + userId = Integer.parseInt(arrayRequestLine[1].substring("/users/".length())); + } catch (Exception e) { + e.printStackTrace(); + } + + return userId; + } +} diff --git a/src/model/User.java b/src/model/User.java deleted file mode 100644 index 548c4fa..0000000 --- a/src/model/User.java +++ /dev/null @@ -1,26 +0,0 @@ -package model; - -public class User { - private String name, job; - - public User(String name, String job) { - this.name = name; - this.job = job; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public String getJob() { - return job; - } - - public void setJob(String job) { - this.job = job; - } -} diff --git a/src/model/Users.java b/src/model/Users.java deleted file mode 100644 index 086ec63..0000000 --- a/src/model/Users.java +++ /dev/null @@ -1,73 +0,0 @@ -package model; - -import java.util.ArrayList; -import java.util.Map; - -public class Users { - private ArrayList data; - - public Users() { - data = new ArrayList<>(); - - data.add(new User("Alex", "Data Analyst")); - data.add(new User("Budi", "Frontend Web Developer")); - data.add(new User("Cahya", "Computer Vision Engineer")); - data.add(new User("Dono", "Data Analyst")); - data.add(new User("Cakra", "Frontend Web Developer")); - data.add(new User("Zara", "Frontend Web Developer")); - data.add(new User("Jojo", "Machine Learning Engineer")); - data.add(new User("Pepe", "Data Analyst")); - } - - public ArrayList getAllData() { - return data; - } - - public int getArrayLength() { - return data.size(); - } - - public User getUserDataByIndex(int index) { - return data.get(index); - } - - public ArrayList getUserDataByQuery(Map queryParams) { - String nameWanted = queryParams.get("name"); - String jobWanted = queryParams.get("job"); - - ArrayList result = new ArrayList<>(); - - if (!nameWanted.equals("") && !jobWanted.equals("")) { - for (User datum : data) { - if (datum.getName().equalsIgnoreCase(nameWanted) && - datum.getJob().equalsIgnoreCase(jobWanted)) { - result.add(datum); - } - } - } else if (!nameWanted.equals("")) { - for (User datum : data) { - if (datum.getName().equalsIgnoreCase(nameWanted)) { - result.add(datum); - } - } - } else if (!jobWanted.equals("")) { - for (User datum : data) { - if (datum.getJob().equalsIgnoreCase(jobWanted)) { - result.add(datum); - } - } - } else { - result = getAllData(); - } - - return result; - } - - public void addNewData(User newData) { - data.add(newData); - } - - public void removeData(int index) { - data.remove(index); - } -} \ No newline at end of file diff --git a/src/test/java/com/server/model/UsersTest.java b/src/test/java/com/server/model/UsersTest.java new file mode 100644 index 0000000..0e12e84 --- /dev/null +++ b/src/test/java/com/server/model/UsersTest.java @@ -0,0 +1,33 @@ +package com.server.model; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.Test; + +public class UsersTest { + + @Test + public void testAddNewData() { + Users users = new Users(); + int initialSize = users.getArrayLength(); + + User newUser = new User("Test User", "Tester"); + users.addNewData(newUser); + + assertEquals(initialSize + 1, users.getArrayLength(), "Users array size should increase by 1"); + assertEquals( + "Test User", users.getUserDataByIndex(initialSize).getName(), "New user name should match"); + assertEquals( + "Tester", users.getUserDataByIndex(initialSize).getJob(), "New user job should match"); + } + + @Test + public void testRemoveData() { + Users users = new Users(); + int initialSize = users.getArrayLength(); + + users.removeData(0); // Remove the first user + + assertEquals(initialSize - 1, users.getArrayLength(), "Users array size should decrease by 1"); + } +} diff --git a/src/utils/HttpUtils.java b/src/utils/HttpUtils.java deleted file mode 100644 index 4382d47..0000000 --- a/src/utils/HttpUtils.java +++ /dev/null @@ -1,103 +0,0 @@ -package utils; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.net.Socket; -import java.util.HashMap; -import java.util.Map; - -public class HttpUtils { - - /** - * Sends a 404 Not Found response to the client. - * - * @param clientSocket the socket connection to the client - * @throws IOException if an I/O error occurs while sending the response - */ - public static void handle404ErrorResponse(Socket clientSocket) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - out.println("HTTP/1.1 404 Not Found"); - out.println("Content-Type: text/html"); - out.println(); - out.println("

404 Not Found!

"); - } - } - - /** - * Sends a 500 Internal Server Error response to the client. - * - * @param clientSocket the socket connection to the client - * @param e the exception that occurred, which will be included in the response message - */ - public static void handleServerErrorResponse(Socket clientSocket, Exception e) { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { - out.println("HTTP/1.1 500 Internal Server Error"); - out.println("Content-Type: text/html"); - out.println(); - out.println("Error occured: " + e.getMessage()); - } catch (IOException ex) { - ex.printStackTrace(); - } - } - - /** - * Retrieves the entire HTTP request body from the given BufferedReader. - * - * @param in the BufferedReader from which the HTTP request body is read - * @return a String containing the entire request body - */ - public static String getRequestBody(BufferedReader in) throws IOException { - String requestBody = null; - int contentLength = getContentLength(in); - - // Read the body content if not empty - if (contentLength > 0) { - char[] body = new char[contentLength]; - try { - in.read(body, 0, contentLength); - } catch (IOException e) { - e.printStackTrace(); - } - requestBody = new String(body); - } - - return requestBody; - } - - private static int getContentLength(BufferedReader in) throws IOException { - String inputLine; - int contentLength = 0; - while ((inputLine = in.readLine()) != null && !inputLine.isEmpty()) { - if (inputLine.startsWith("Content-Length:")) { - contentLength = Integer.parseInt(inputLine.split(":")[1].trim()); - } - } - - return contentLength; - } - - /** - * Extracts raw query parameters from a URL string and returns them as a map. - * - * @param queryParam the URL query parameter string (e.g., "key1=value1&key2=value2") - * @return a Map containing the extracted query parameters, where each key is a parameter name - * and the corresponding value is the parameter value - */ - public static Map extractQueryParams(String queryParam) { - Map result = new HashMap<>(); - String[] pairs = queryParam.split("&"); - - for (String pair : pairs) { - String[] keyValue = pair.split("="); - if (keyValue.length == 2) { - result.put(keyValue[0], keyValue[1]); - } else if (keyValue.length == 1) { - result.put(keyValue[0], ""); - } - } - - return result; - } - -} diff --git a/src/utils/UserUtils.java b/src/utils/UserUtils.java deleted file mode 100644 index 999617d..0000000 --- a/src/utils/UserUtils.java +++ /dev/null @@ -1,23 +0,0 @@ -package utils; - -public class UserUtils { - - /** - * Retrieves the user ID from the specified URL request line. - * - * @param requestLine the HTTP request line containing the URL with the user ID parameter - * @return the user ID extracted from the request line - */ - public static int getUserId(String requestLine) { - String[] arrayRequestLine = requestLine.split(" "); - int userId = -1; - - try { - userId = Integer.parseInt(arrayRequestLine[1].substring("/users/".length())); - } catch (Exception e) { - e.printStackTrace(); - } - - return userId; - } -}