diff --git a/README.md b/README.md index b1cccfd..57bed41 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,228 @@ -## Submission Instructions - -To submit your Oracle JAVA Spring Boot Maven project as a solution, please follow these steps: - -### Step 1: Install git on your PC -- Install "git" as shown in this tutorial: [How to install git](https://youtu.be/iYkLrXobBbA?si=_l0haibv_X9NpIjJ) -- Open command prompt and run - ```bash - git version - ``` -- If you see the version, then git is successfully installed. - -### Step 2: Fork the Repository -- Navigate to [this repository](https://github.com/CodelineAtyab/oraclequantapi) provided by Codeline. -- Click on the "Fork" button at the top-right corner of the page to create a copy of the repository under your own GitHub account. - -### Step 3: Clone the Forked Repository -- Open your terminal or command prompt. -- Clone the forked repository to your local machine using the following command: - ```bash - git clone https://github.com/your-username/repo-name.git - ``` - -### Step 4: Create a new branch -- Navigate to the cloned repository directory - ```bash - cd repo-name - ``` -- Create a new branch for your code submissions (Replace your-name with your name in your-name-submission-branch): - ```bash - git checkout -b your-name-submission-branch - ``` - - -### Step 5: Add Your Code -- Implement the API - -### Step 6: Commit your changes -- Run the following commands in order to commit your changes: - ```bash - git add * - git commit -m "Meaningful commit message here" - ``` - -### Step 7: Push Your Branch to GitHub -- Run the following commands to upload the changes to the forked github repository (Replace your-name with your name in your-name-submission-branch): - ```bash - git push origin your-name-submission-branch - ``` - -### Step 8: Create a Pull Request -- Go to your forked repository on GitHub. -- You should see a prompt to create a pull request. Click on "Compare & pull request". -- Provide a title and description for your pull request, then click "Create pull request". - -### Step 9: Notify Codeline -- Notify on slack that you have created a PR for your solution. - -## Note: If you face any issues in the process above, Please do the following: -- Watch [this youtube tutorial](https://www.youtube.com/watch?v=a_FLqX3vGR4) -- Contact Ikhlas or Atyab. +# OracleQuant ERP - Package Measurement Conversion API + +Trainer-style Spring Boot API for converting encoded package measurement strings into package inflow totals. The application uses Java 17, Spring Boot, Maven, Oracle Linux, and Oracle XE. + +## Features + +- `GET /convert-measurements` converts encoded measurement input into a JSON array. +- Conversion history is stored in Oracle XE through Spring Data JPA. +- History can be fetched, updated, and cleared through REST endpoints. +- Console logs and rolling file logs are enabled. +- Log files keep one week of history. + +## Project Structure + +```text +src/main/java/om/oraclequant/pkc_api/OracleQuantPkcApiApplication.java +src/main/java/om/oraclequant/pkc_api/controllers/MeasurementController.java +src/main/java/om/oraclequant/pkc_api/controllers/HistoryController.java +src/main/java/om/oraclequant/pkc_api/services/MeasurementService.java +src/main/java/om/oraclequant/pkc_api/services/HistoryService.java +src/main/java/om/oraclequant/pkc_api/models/MeasurementSequence.java +src/main/java/om/oraclequant/pkc_api/models/MeasurementHistory.java +src/main/java/om/oraclequant/pkc_api/repositories/MeasurementHistoryRepository.java +src/main/resources/application.properties +version.txt +``` + +## Conversion Rules + +- `a` to `z` represent `1` to `26`. +- `_` represents `0`. +- Uppercase letters are converted to lowercase. +- Characters other than letters and `_` are converted to `_`, so they count as zero. +- A sequence of `z` characters continues until the first non-`z` character. +- Each package starts with an encoded count, then up to that many encoded measurement values. +- If the count asks for more values than remain, the package uses only the remaining values. + +Examples: + +```text +aa -> [1] +abbcc -> [2, 6] +dz_a_aazzaaa -> [28, 53, 1] +a_ -> [0] +abcdabcdab -> [2, 7, 7] +abcdabcdab_ -> [2, 7, 7, 0] +zdaaaaaaaabaaaaaaaabaaaaaaaabbaa -> [34] +za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa -> [40, 1] +a1 -> [0] +``` + +## Database Configuration + +The application is configured for Oracle XE using Oracle Thin JDBC. + +Edit `src/main/resources/application.properties`: + +```properties +spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1 +spring.datasource.username=your_username +spring.datasource.password=your_password +spring.datasource.driver-class-name=oracle.jdbc.OracleDriver + +spring.jpa.hibernate.ddl-auto=update +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect +spring.jpa.show-sql=true +``` + +For Oracle XE, the common service name is: + +```text +XEPDB1 +``` + +If your Oracle installation uses SID instead, the URL may look like: + +```properties +spring.datasource.url=jdbc:oracle:thin:@localhost:1521:XE +``` + +## Running The Application + +From the project root: + +```bash +./mvnw spring-boot:run +``` + +Or build and run the jar: + +```bash +./mvnw clean package +java -jar target/pkc-api.jar +``` + +The API runs on: + +```text +http://localhost:8080 +``` + +## REST API Endpoints + +Convert measurements: + +```bash +curl 'http://localhost:8080/convert?input=aa' +``` + +Response: + +```json +[1] +``` + +The older query parameter name is also supported: + +```bash +curl 'http://localhost:8080/convert?convert=aa' +``` + +Get all history records: + +```bash +curl 'http://localhost:8080/history' +``` + +Get one history record: + +```bash +curl 'http://localhost:8080/history/{id}' +``` + +Update one history record: + +```bash +curl -X PUT 'http://localhost:8080/history/{id}' \ + -H 'Content-Type: application/json' \ + -d '{"input":"aa","output":"[1]"}' +``` + +Clear all history: + +```bash +curl -X DELETE 'http://localhost:8080/history' +``` + +## Local No-Database Test + +Before connecting Oracle, the conversion logic can be tested locally with: + +```text +local-test/MeasurementLocalTest.java +``` + +This runner checks the conversion rules without starting Spring Boot and without using Oracle. + +## Deploy On Oracle Linux Via SSH + +Build the jar locally: + +```bash +./mvnw clean package +``` + +Copy the jar to Oracle Linux: + +```bash +scp target/pkc-api.jar opc@your-server-ip:/home/opc/pkc-api.jar +``` + +SSH into the Oracle Linux server: + +```bash +ssh opc@your-server-ip +``` + +Install Java 17 if needed: + +```bash +sudo dnf install -y java-17-openjdk +``` + +Set database environment values or edit `application.properties` before building: + +```bash +export ORACLE_DB_URL='jdbc:oracle:thin:@localhost:1521/XEPDB1' +export ORACLE_DB_USERNAME='your_username' +export ORACLE_DB_PASSWORD='your_password' +``` + +Run the jar: + +```bash +java -jar /home/opc/pkc-api.jar +``` + +Optional systemd service: + +```ini +[Unit] +Description=OracleQuant Package Measurement API +After=network.target + +[Service] +User=opc +WorkingDirectory=/home/opc +ExecStart=/usr/bin/java -jar /home/opc/pkc-api.jar +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +Save it as: + +```text +/etc/systemd/system/pkc-api.service +``` + +Then run: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now pkc-api +sudo systemctl status pkc-api +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 20909d2..d8f7b08 100644 --- a/pom.xml +++ b/pom.xml @@ -10,7 +10,7 @@ com.oraclequantapi oraclequantapi - 0.0.1-SNAPSHOT + 1.0.0 @@ -35,11 +35,29 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.oracle.database.jdbc + ojdbc11 + runtime + + org.springframework.boot spring-boot-starter-test test + + + com.h2database + h2 + test + + diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controllers/HistoryController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/HistoryController.java new file mode 100644 index 0000000..c1f4e3a --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/HistoryController.java @@ -0,0 +1,48 @@ +package com.oraclequantapi.oraclequantapi.controllers; + +import com.oraclequantapi.oraclequantapi.models.MeasurementHistory; +import com.oraclequantapi.oraclequantapi.services.HistoryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping (path = "/history") +public class HistoryController { + @Autowired + public HistoryService historyService; + + // Return all saved conversion requests from Oracle DB. + @GetMapping + public ResponseEntity> getAllHistory() { + return ResponseEntity.status(HttpStatus.OK).body(historyService.getHistory()); + } + + // Return one saved conversion request by id. + @GetMapping(path = "/{id}") + public ResponseEntity getSpecificHistory(@PathVariable String id) { + MeasurementHistory history = historyService.getHistoryById(id); + return ResponseEntity.status(HttpStatus.OK).body(history); + } + + // Update a history record by id. + @PutMapping(path = "/{id}") + public ResponseEntity updateHistory( + @PathVariable String id, + @RequestBody MeasurementHistory incomingHistory + ) { + MeasurementHistory history = historyService.updateHistory(id, incomingHistory); + return ResponseEntity.status(HttpStatus.OK).body(history); + } + + // Delete all saved history records. + @DeleteMapping + public ResponseEntity clearHistory() { + historyService.clearHistory(); + return ResponseEntity.status(HttpStatus.NO_CONTENT).build(); + } + +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controllers/MeasurementController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/MeasurementController.java new file mode 100644 index 0000000..f834454 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/MeasurementController.java @@ -0,0 +1,42 @@ +package com.oraclequantapi.oraclequantapi.controllers; + +import com.oraclequantapi.oraclequantapi.services.MeasurementService; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class MeasurementController { + @Autowired + public MeasurementService measurementService; + + // /convert-measurements?input=aa + @GetMapping(path = "/convert") + public ResponseEntity> convertMeasurements( + @RequestParam(name = "input", required = false) String input, + @RequestParam(name = "convert", required = false) String legacyInput, + HttpServletRequest request + ) { + + String selectedInput = input != null ? input : legacyInput; + List output = measurementService.convertAndSave(selectedInput, getSourceIpAddress(request)); + return ResponseEntity.status(HttpStatus.OK).body(output); + } + + // Get the real client IP when the app is behind a proxy; otherwise use remote address. + private String getSourceIpAddress(HttpServletRequest request) { + String forwardedFor = request.getHeader("X-Forwarded-For"); + + if (forwardedFor != null && !forwardedFor.isBlank()) { + return forwardedFor.split(",")[0].trim(); + } + + return request.getRemoteAddr(); + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/models/MeasurementHistory.java b/src/main/java/com/oraclequantapi/oraclequantapi/models/MeasurementHistory.java new file mode 100644 index 0000000..e4e7341 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/models/MeasurementHistory.java @@ -0,0 +1,38 @@ +package com.oraclequantapi.oraclequantapi.models; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "MEASUREMENT_TABLE") +public class MeasurementHistory { + + // UUID string + @Id + @Column(name = "ID", length = 36) + public String id; + + // Time when the API received the conversion request. + @Column(name = "REQUEST_TIMESTAMP") + public LocalDateTime timestamp; + + // Client IP address. JsonProperty makes the JSON name match the task wording. + @JsonProperty("source_ip_address") + @Column(name = "SOURCE_IP_ADDRESS") + public String sourceIpAddress; + + + // Normalized input string after converting capital letters to lowercase. + // Lob: Large Object & No size limit + @Lob + @Column(name = "MEASUREMENT_INPUT") + public String input; + + // Output is stored as text, for example "[2,6]". + @Lob + @Column(name = "MEASUREMENT_OUTPUT") + public String output; + +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/models/MeasurementSequence.java b/src/main/java/com/oraclequantapi/oraclequantapi/models/MeasurementSequence.java new file mode 100644 index 0000000..9061125 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/models/MeasurementSequence.java @@ -0,0 +1,49 @@ +package com.oraclequantapi.oraclequantapi.models; + + +import java.util.Locale; + +public class MeasurementSequence { + + public String value; + + public MeasurementSequence(String value){ + setValue(value); + } + + // capital letters converted into lowercases + // Anything than letters and underscore will be converted into underscore + private void setValue(String value) { + if (value == null) { + this.value = null; + } else { + this.value = normalize(value); + } + } + + public String getValueAsString() { + return value; + } + + // After normalization, the value is valid as long as it is not null. + public boolean isValid() { + return value != null; + } + + + private String normalize (String value){ + String lowerValue = value.toLowerCase(Locale.ROOT); + StringBuilder normalizedValue = new StringBuilder(); + + for (int index = 0; index < lowerValue.length(); index++) { + char current = lowerValue.charAt(index); + + if ((current >= 'a' && current <= 'z') || current == '_') { + normalizedValue.append(current); + }else { + normalizedValue.append('_'); + } + } + return normalizedValue.toString(); + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/repositories/MeasurementHistoryRepository.java b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/MeasurementHistoryRepository.java new file mode 100644 index 0000000..a4ca7d4 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/MeasurementHistoryRepository.java @@ -0,0 +1,10 @@ +package com.oraclequantapi.oraclequantapi.repositories; + +import com.oraclequantapi.oraclequantapi.models.MeasurementHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MeasurementHistoryRepository extends JpaRepository { + // JpaRepository already provides: findAll, findById, save, deleteById, deleteAll. +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/services/HistoryService.java b/src/main/java/com/oraclequantapi/oraclequantapi/services/HistoryService.java new file mode 100644 index 0000000..1863641 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/services/HistoryService.java @@ -0,0 +1,75 @@ +package com.oraclequantapi.oraclequantapi.services; + +import com.oraclequantapi.oraclequantapi.models.MeasurementHistory; +import com.oraclequantapi.oraclequantapi.models.MeasurementSequence; +import com.oraclequantapi.oraclequantapi.repositories.MeasurementHistoryRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; + +@Service +public class HistoryService { + + @Autowired + private MeasurementHistoryRepository measurementHistoryRepository; + + // Save one conversion request into Oracle DB + public MeasurementHistory saveCurrentMeasurement( + String sourceIpAddress, + MeasurementSequence sequence, + List output + ) { + MeasurementHistory history = new MeasurementHistory(); + history.id = UUID.randomUUID().toString(); + history.timestamp = LocalDateTime.now(); + history.sourceIpAddress = sourceIpAddress; + history.input = sequence.getValueAsString(); + history.output = formatOutput(output); + return measurementHistoryRepository.save(history); + } + + public List getHistory() { + return measurementHistoryRepository.findAll(); + } + + public MeasurementHistory getHistoryById(String id) { + return measurementHistoryRepository.findById(id).orElse(null); + } + // PUT uses this method. Only non-null fields are changed. + public MeasurementHistory updateHistory(String id, MeasurementHistory givenHistory) { + MeasurementHistory existingHistory = getHistoryById(id); + + if (existingHistory == null) { + return null; + } + + if (givenHistory.sourceIpAddress != null) { + existingHistory.sourceIpAddress = givenHistory.sourceIpAddress; + } + + if (givenHistory.input != null) { + existingHistory.input = givenHistory.input; + } + + if (givenHistory.output != null) { + existingHistory.output = givenHistory.output; + } + + return measurementHistoryRepository.save(existingHistory); + } + + public void clearHistory() { + measurementHistoryRepository.deleteAll(); + } + + // Converts List into text before storing it in Oracle DB. + private String formatOutput(List output) { + return output.stream() + .map(String::valueOf) + .collect(Collectors.joining(",", "[", "]")); + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/services/MeasurementService.java b/src/main/java/com/oraclequantapi/oraclequantapi/services/MeasurementService.java new file mode 100644 index 0000000..684a35c --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/services/MeasurementService.java @@ -0,0 +1,108 @@ +package com.oraclequantapi.oraclequantapi.services; + +import com.oraclequantapi.oraclequantapi.models.MeasurementSequence; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.server.ResponseStatusException; + +import java.util.ArrayList; +import java.util.List; + +@Service +public class MeasurementService { + + @Autowired + private HistoryService historyService; + + // Main method used by the controller: convert the input, then save the request in Oracle DB. + public List convertAndSave(String input, String sourceIpAddress) { + MeasurementSequence sequence = getMeasurement(input); + List output = processMeasurement(sequence); + historyService.saveCurrentMeasurement(sourceIpAddress, sequence, output); + return output; + } + + // Build a MeasurementSequence object from the raw query parameter. + public MeasurementSequence getMeasurement(String givenInput) { + if (givenInput == null) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Query parameter 'input' is required."); + } + + MeasurementSequence sequence = new MeasurementSequence(givenInput); + + if (!sequence.isValid()) { + throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Query parameter 'input' is required."); + } + + return sequence; + } + + // Converts the encoded string into package totals. + public List processMeasurement(MeasurementSequence sequence) { + String input = sequence.getValueAsString(); + List results = new ArrayList<>(); + int index = 0; + + while (index < input.length()) { + // Every package starts with an encoded count. + ReadResult countResult = readEncodedNumber(input, index); + long count = countResult.value; + index = countResult.nextIndex; + + long packageTotal = 0; + long valuesRead = 0; + + // Read up to "count" measurement values. If the string ends early, use what exists. + while (valuesRead < count && index < input.length()) { + ReadResult measurementResult = readEncodedNumber(input, index); + packageTotal = packageTotal + measurementResult.value; + index = measurementResult.nextIndex; + valuesRead++; + } + + results.add(packageTotal); + } + + return results; + } + + // Reads one encoded number. + // Examples: "_" = 0, "a" = 1, "z" = 26, "za" = 27, "zza" = 53. + private ReadResult readEncodedNumber(String input, int startIndex) { + long value = 0; + int index = startIndex; + + // Keep adding 26 for every z until the first non-z character appears. + while (index < input.length() && input.charAt(index) == 'z') { + value = value + 26; + index++; + } + + // If the input ended after z characters, return the total z value. + if (index >= input.length()) { + return new ReadResult(value, index); + } + + char current = input.charAt(index); + + if (current == '_') { + return new ReadResult(value, index + 1); + } + + value = value + (current - 'a' + 1); + return new ReadResult(value, index + 1); + } + + // Helper object that returns both the decoded value and the next reading position + private static class ReadResult { + public long value; + public int nextIndex; + + public ReadResult(long value, int nextIndex) { + this.value = value; + this.nextIndex = nextIndex; + } + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 99d0060..de13d21 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,14 @@ -spring.application.name=oraclequantapi +spring.application.name=oraclequant-api + +# Oracle XE datasource +spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1 +spring.datasource.username=system +spring.datasource.password=29999login +spring.datasource.driver-class-name=oracle.jdbc.OracleDriver + + +# JPA / Hibernate settings +# "update" creates/updates tables automatically — safe for development +spring.jpa.hibernate.ddl-auto=update +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect +spring.jpa.show-sql=true diff --git a/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java b/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java index 2de285b..fd1f606 100644 --- a/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java +++ b/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java @@ -1,9 +1,16 @@ package com.oraclequantapi.oraclequantapi; import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; @SpringBootTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.ANY) +@TestPropertySource(properties = { + "spring.jpa.database-platform=org.hibernate.dialect.H2Dialect", + "spring.jpa.hibernate.ddl-auto=update" +}) class OraclequantapiApplicationTests { @Test diff --git a/tatus b/tatus new file mode 100644 index 0000000..851de95 --- /dev/null +++ b/tatus @@ -0,0 +1,17 @@ +commit 56b42d8cbe52fbf5e5c09c6b3fe33f4cff5a6a4b (HEAD -> ibrahim-submission-branch, origin/main, origin/HEAD, main) +Author: Syed Atyab Hussain <119934383+CodelineAtyab@users.noreply.github.com> +Date: Wed May 20 12:18:04 2026 +0400 + + Update README.md + +commit 9dd10622247ad669593fea7420d47916d339b153 +Author: Syed Atyab Hussain +Date: Wed May 20 12:11:45 2026 +0400 + + Initialized and empty SpringBoot Project + +commit daa75b6e7f160ad42127809aa6371a104373343b +Author: Syed Atyab Hussain +Date: Wed May 20 12:08:50 2026 +0400 + + first commit