From 96f279d7ffd9f0e5ab2b7c9b315f86e1bb83531a Mon Sep 17 00:00:00 2001 From: Farrel Augusta Dinata Date: Wed, 23 Sep 2026 00:12:01 +0700 Subject: [PATCH 1/4] refactor: standardize Java project structure, add tooling and CI/CD --- .github/workflows/deploy.yml | 82 +++++ .github/workflows/pr-check.yml | 26 ++ .gitignore | 48 ++- pom.xml | 74 ++++ src/Main.java | 63 ---- src/controller/UsersController.java | 313 ---------------- src/main/java/com/server/Main.java | 70 ++++ .../server}/controller/RootController.java | 53 +-- .../server/controller/UsersController.java | 335 ++++++++++++++++++ src/main/java/com/server/model/User.java | 26 ++ src/main/java/com/server/model/Users.java | 73 ++++ src/main/java/com/server/utils/HttpUtils.java | 102 ++++++ src/main/java/com/server/utils/UserUtils.java | 23 ++ src/model/User.java | 26 -- src/model/Users.java | 73 ---- src/test/java/com/server/model/UsersTest.java | 33 ++ src/utils/HttpUtils.java | 103 ------ src/utils/UserUtils.java | 23 -- 18 files changed, 917 insertions(+), 629 deletions(-) create mode 100644 .github/workflows/deploy.yml create mode 100644 .github/workflows/pr-check.yml create mode 100644 pom.xml delete mode 100644 src/Main.java delete mode 100644 src/controller/UsersController.java create mode 100644 src/main/java/com/server/Main.java rename src/{ => main/java/com/server}/controller/RootController.java (68%) create mode 100644 src/main/java/com/server/controller/UsersController.java create mode 100644 src/main/java/com/server/model/User.java create mode 100644 src/main/java/com/server/model/Users.java create mode 100644 src/main/java/com/server/utils/HttpUtils.java create mode 100644 src/main/java/com/server/utils/UserUtils.java delete mode 100644 src/model/User.java delete mode 100644 src/model/Users.java create mode 100644 src/test/java/com/server/model/UsersTest.java delete mode 100644 src/utils/HttpUtils.java delete mode 100644 src/utils/UserUtils.java 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/pom.xml b/pom.xml new file mode 100644 index 0000000..5a5482d --- /dev/null +++ b/pom.xml @@ -0,0 +1,74 @@ + + + 4.0.0 + + com.server + basic-web-server + 1.0-SNAPSHOT + + + 17 + 17 + + + + + + org.junit.jupiter + junit-jupiter + 5.10.0 + test + + + + + + + + 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/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/controller/RootController.java b/src/main/java/com/server/controller/RootController.java similarity index 68% rename from src/controller/RootController.java rename to src/main/java/com/server/controller/RootController.java index 048c4e4..7e39322 100644 --- a/src/controller/RootController.java +++ b/src/main/java/com/server/controller/RootController.java @@ -1,53 +1,54 @@ -package controller; +package com.server.controller; +import com.server.model.Users; import java.io.IOException; import java.io.PrintWriter; import java.net.Socket; -import model.Users; - public class RootController { - private Users data; + private Users data; - public RootController(Users data) { - this.data = data; - } + public RootController(Users data) { + this.data = data; + } - public void handleGetRequest(Socket clientSocket) throws IOException { - try (PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true)) { + 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 header + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: text/html"); + out.println(); - // HTTP response body - out.println( - """ + // HTTP response body + out.println( + """ - + Root Page - +

Welcome! You successfully get response from server

-

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

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

- +
- +

Send user data to server!

- +

- +

Search user data!

@@ -60,9 +61,9 @@ public void handleGetRequest(Socket clientSocket) throws IOException {

*) Empty search box will get all user data

- + """); - } } -} \ No newline at end of file + } +} 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..67a61b9 --- /dev/null +++ b/src/main/java/com/server/controller/UsersController.java @@ -0,0 +1,335 @@ +package com.server.controller; + +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; + +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: +

    +
  • Name: """ + + data.getUserDataByIndex(userId - 1).getName() + + """ +
  • +
  • Job: """ + + data.getUserDataByIndex(userId - 1).getJob() + + """ +
  • +
+

+ +

+
+
+

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" + } + """); + } + } +} 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..dcc118d --- /dev/null +++ b/src/main/java/com/server/utils/HttpUtils.java @@ -0,0 +1,102 @@ +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: 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/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; - } -} From bdd84468bf7f9c9a7243c3f411d23bc09d1a3ba8 Mon Sep 17 00:00:00 2001 From: Farrel Augusta Dinata Date: Wed, 23 Sep 2026 08:35:12 +0700 Subject: [PATCH 2/4] refactor: setup to Maven REST API with CI/CD and tooling --- README.md | 96 +++--- pom.xml | 7 + .../com/server/controller/RootController.java | 67 ++-- .../server/controller/UsersController.java | 286 +++--------------- src/main/java/com/server/utils/HttpUtils.java | 12 +- 5 files changed, 132 insertions(+), 336 deletions(-) diff --git a/README.md b/README.md index f0c5cab..ca51584 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,63 @@ 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 - ``` - -The server will be running at http://localhost:8000 +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` +### 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 +``` -## Endpoints -Here are the key endpoints that you can interact with: +## Tooling & Testing -``` -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 +Run the automated tests: +```bash +mvn test ``` -## Query Parameters -Example: /search?name=John&job=data+analyst -Query parameters can be extracted and used in request processing. +Format the codebase and check for linting errors: +```bash +mvn spotless:apply checkstyle:check +``` -## HTTP Methods -The service supports the following HTTP methods: +## Endpoints +Here are the key REST endpoints that you can interact with (use Postman or `curl`): ``` -GET: Retrieve data. -POST: Submit new data. -PATCH: Update partial existing data. -DELETE: Remove data. +GET / : Returns a JSON health check and API status +GET /users : Fetch all users as a JSON array +GET /users/:id : Fetch a specific user by their ID +POST /users : Add a new user (Accepts JSON body: {"name": "...", "job": "..."}) +PATCH /users/:id : Update user information (Accepts JSON body: {"name": "..."}) +DELETE /users/:id : Delete a user by their ID +GET /search?name=&job= : Filters users and returns a JSON array ``` ## 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 index 5a5482d..a48dc8f 100644 --- a/pom.xml +++ b/pom.xml @@ -21,6 +21,13 @@ 5.10.0 test + + + + com.google.code.gson + gson + 2.10.1 + diff --git a/src/main/java/com/server/controller/RootController.java b/src/main/java/com/server/controller/RootController.java index 7e39322..adcb3a0 100644 --- a/src/main/java/com/server/controller/RootController.java +++ b/src/main/java/com/server/controller/RootController.java @@ -1,69 +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: text/html"); + out.println("Content-Type: application/json"); 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

-
- - - - """); + 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 index 67a61b9..39e0b1f 100644 --- a/src/main/java/com/server/controller/UsersController.java +++ b/src/main/java/com/server/controller/UsersController.java @@ -1,5 +1,7 @@ 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; @@ -13,8 +15,10 @@ 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; @@ -22,182 +26,25 @@ public UsersController(Users 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("Content-Type: application/json"); out.println(); - - // HTTP response body - out.println( - """ - - - Welcome! - - -

This is all users data

- """ - + result - + """ - - - """); + 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); - String result = null; if (userId != -1 && userId <= data.getArrayLength()) { - result = - """ -

You get data from the server

-

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

-

Data: -

    -
  • Name: """ - + data.getUserDataByIndex(userId - 1).getName() - + """ -
  • -
  • Job: """ - + data.getUserDataByIndex(userId - 1).getJob() - + """ -
  • -
-

- -

-
-
-

Update this user data!

-
- -
- -
- -
-
- -

- -
-

Delete this user data!

-
- -
-
-
- - - """; - } - - if (result != null) { - // HTTP response header + User user = data.getUserDataByIndex(userId - 1); out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); + out.println("Content-Type: application/json"); out.println(); - - // HTTP response body - out.println( - """ - - - User Data - - - """ - + result - + """ - - - """); + out.println(gson.toJson(user)); } else { HttpUtils.handle404ErrorResponse(clientSocket); } @@ -214,122 +61,83 @@ public void getUserDataByQuery(Socket clientSocket, String requestLine) 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("Content-Type: application/json"); out.println(); - - // HTTP response body - out.println( - """ -

Search result!

- """ + dataHTMLContent); + 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); - String newName = HttpUtils.extractQueryParams(requestBody).get("name"); - String newJob = HttpUtils.extractQueryParams(requestBody).get("job"); - data.addNewData(new User(newName, newJob)); + User newUser = gson.fromJson(requestBody, User.class); + data.addNewData(newUser); - // HTTP response header - out.println("HTTP/1.1 200 OK"); - out.println("Content-Type: text/html"); - out.println(); + JsonObject response = new JsonObject(); + response.addProperty("status", "success"); + response.addProperty("message", "User added successfully"); - // HTTP response body - out.println( - """ -

Data successfully submitted to server

- - - - """); + 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); - 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 (userId == -1 || userId > data.getArrayLength()) { + HttpUtils.handle404ErrorResponse(clientSocket); + return; } - if (!newJob.equals("") && newJob != null) { - data.getUserDataByIndex(userId - 1).setJob(newJob); + 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()); } - // HTTP response header + 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(); - - // HTTP response body - out.println( - """ - { - "status": "success", - "message": "Data updated successfully" - } - """); + out.println(gson.toJson(response)); } } public void deleteUserDataById(Socket clientSocket, String requestLine) throws IOException { - System.out.println("delete bang!"); + 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); - // HTTP response header + 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(); - - // HTTP response body - out.println( - """ - { - "status": "success", - "message": "Data deleted successfully" - } - """); + out.println(gson.toJson(response)); } } } diff --git a/src/main/java/com/server/utils/HttpUtils.java b/src/main/java/com/server/utils/HttpUtils.java index dcc118d..57ff863 100644 --- a/src/main/java/com/server/utils/HttpUtils.java +++ b/src/main/java/com/server/utils/HttpUtils.java @@ -18,9 +18,9 @@ public class HttpUtils { 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("Content-Type: application/json"); out.println(); - out.println("

404 Not Found!

"); + out.println("{\"error\": \"404 Not Found\"}"); } } @@ -33,9 +33,13 @@ public static void handle404ErrorResponse(Socket clientSocket) throws IOExceptio 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("Content-Type: application/json"); out.println(); - out.println("Error occured: " + e.getMessage()); + + // 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(); } From 15c4d93480a96720d48f17455d086bca7cc53f0e Mon Sep 17 00:00:00 2001 From: Farrel Augusta Dinata Date: Wed, 23 Sep 2026 08:41:03 +0700 Subject: [PATCH 3/4] chore: add fast pre-commit hooks for Spotless and Checkstyle --- .githooks/pre-commit | 13 +++++++++++++ README.md | 8 ++++++++ 2 files changed, 21 insertions(+) create mode 100644 .githooks/pre-commit 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/README.md b/README.md index ca51584..1fa7d5e 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,14 @@ Format the codebase and check for linting errors: mvn spotless:apply checkstyle:check ``` +### 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. + ## Endpoints Here are the key REST endpoints that you can interact with (use Postman or `curl`): From 4049ccbe123b247dfa1b23ae66cbf8c6975c082f Mon Sep 17 00:00:00 2001 From: Farrel Augusta Dinata Date: Wed, 23 Sep 2026 08:43:09 +0700 Subject: [PATCH 4/4] docs: add HTTP request and response payload examples to README --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 84 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 1fa7d5e..1598104 100644 --- a/README.md +++ b/README.md @@ -65,17 +65,91 @@ git config core.hooksPath .githooks Every time you commit, it will automatically verify formatting and linting. If it fails, the commit will be aborted. ## Endpoints -Here are the key REST endpoints that you can interact with (use Postman or `curl`): +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 + } + ``` -``` -GET / : Returns a JSON health check and API status -GET /users : Fetch all users as a JSON array -GET /users/:id : Fetch a specific user by their ID -POST /users : Add a new user (Accepts JSON body: {"name": "...", "job": "..."}) -PATCH /users/:id : Update user information (Accepts JSON body: {"name": "..."}) -DELETE /users/:id : Delete a user by their ID -GET /search?name=&job= : Filters users and returns a JSON array -``` +### 2. Fetch All Users +- **`GET /users`** + - **Response (200 OK):** + ```json + [ + { "name": "Alex", "job": "Data Analyst" }, + { "name": "Budi", "job": "Frontend Web Developer" } + ] + ``` + +### 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 uses the standard Maven structure: