diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..f2d6528 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,60 @@ +# Changelog + +All notable project changes are documented in this file. + +## 0.0.1-SNAPSHOT - 2026-05-21 + +- Created the initial Spring Boot Maven project. +- Added the basic application entry point. +- Added the initial README file. + +## 0.0.2-SNAPSHOT - 2026-05-21 + +- Added the package measurement conversion REST API. +- Implemented conversion logic for `_`, `a-z`, and multi-character `z` values. +- Added request history persistence with `id`, `timestamp`, `source_ip_address`, `input`, and `output` fields. +- Added history REST endpoints for `GET`, `PUT`, `PATCH`, and `DELETE` requests. +- Added Oracle XE database configuration through the `oracle` Spring profile. +- Added local H2 database support for development and automated tests. +- Added Logback configuration for console logging and daily rolling file logging. +- Configured application logs to write to `logs/oraclequantapi.log` by default. +- Configured rolling log retention for seven days. +- Added unit and integration tests for conversion and API behavior. + +## 0.0.3-SNAPSHOT - 2026-05-21 + +- Refactored the Java source structure into `controller`, `service`, `model`, and `repository` packages. +- Renamed the main application class to `OracleQuantApiApplication`. +- Renamed service and repository classes to match the requested project structure. +- Updated unit and integration tests to match the new package structure. + +## 0.0.4-SNAPSHOT - 2026-05-22 + +- Updated conversion behavior so invalid characters are treated as `0` instead of returning errors. +- Updated conversion behavior so missing measured values are treated as `0`. +- Added support for empty or missing `input` values, returning an empty list. +- Updated history persistence to support empty request input values. +- Updated tests for tolerant conversion behavior. + +## 0.0.5-SNAPSHOT - 2026-05-22 + +- Reviewed the current project structure and database-related files. +- Added `ConversionHistory` entity file for conversion history data modeling. +- Added `ConversionHistoryRepository` for conversion history database access. +- Kept the active API history flow using `HistoryRecord`, `HistoryRepository`, and `HistoryService`. +- Updated `CHANGELOG.md` and `version.txt` to match the current project state. + +## 0.0.6-SNAPSHOT - 2026-05-23 + +- Updated logging documentation in `CHANGELOG.md` and `version.txt`. +- Documented that the application uses Logback for console and file logging. +- Documented that logs are written to `logs/oraclequantapi.log` by default. +- Documented that rolling log files are retained for seven days. + +## 0.0.7-SNAPSHOT - 2026-05-25 + +- Updated documentation for `application-oracle.properties` and `logback-spring.xml`. +- Documented that `application-oracle.properties` stores Oracle XE profile database settings separately from local H2 settings. +- Documented that the Oracle profile can be activated with `SPRING_PROFILES_ACTIVE=oracle`. +- Documented that `logback-spring.xml` controls console logging, file logging, daily log rotation, and seven-day log retention. +- Clarified that logs are written to `logs/oraclequantapi.log` by default. \ No newline at end of file diff --git a/README.md b/README.md index b1cccfd..8e53aea 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,498 @@ -## 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 Package Measurement Conversion API + +Spring Boot REST API for converting package measurement strings into package totals and storing request history. + +## Requirements + +- Oracle OpenJDK 17 +- Maven 3.9+ or the included Maven wrapper +- Docker if running Oracle XE locally in a container +- Oracle XE Database for Oracle profile usage +- Oracle Linux host for final deployment + +## Build The Application + +Windows PowerShell: + +```powershell +.\mvnw.cmd clean package +``` + +Linux or macOS: + +```bash +./mvnw clean package +``` + +The runnable jar is created at: + +```text +target/oraclequantapi-0.0.1-SNAPSHOT.jar +``` + +## Running The Application + +The default configuration uses an in-memory H2 database. This is useful for local development and testing because Oracle XE is not required. + +Run with Maven on Windows: + +```powershell +.\mvnw.cmd spring-boot:run +``` + +Run the packaged jar on Windows: + +```powershell +java -jar target\oraclequantapi-0.0.1-SNAPSHOT.jar +``` + +Run with Maven on Linux: + +```bash +./mvnw spring-boot:run +``` + +Run the packaged jar on Linux: + +```bash +java -jar target/oraclequantapi-0.0.1-SNAPSHOT.jar +``` + +The API starts at: + +```text +http://localhost:8080 +``` + +The dashboard is available at: + +```text +http://localhost:8080/ +``` + +Run tests: + +```powershell +.\mvnw.cmd test +``` + +## Dashboard + +The application includes a built-in dashboard served by Spring Boot from: + +```text +src/main/resources/static +``` + +Dashboard files: + +```text +src/main/resources/static/index.html +src/main/resources/static/styles.css +src/main/resources/static/app.js +``` + +Dashboard features: + +- Convert package measurement strings from the browser. +- Show conversion results. +- Load request history from the active database. +- Refresh request history. +- Delete one history record. +- Delete all history records. +- Show a clear warning if the Oracle `/history` endpoint fails because of database user or table schema problems. + +The dashboard uses the same REST endpoints documented below. + +## Configuring The Database + +The application supports two database modes. + +Default local mode: + +- Uses H2 in-memory database. +- Does not require external database setup. +- Configured in `src/main/resources/application.properties`. +- Good for development and tests. + +Oracle XE mode: + +- Uses Oracle XE through the `oracle` Spring profile. +- Configured in `src/main/resources/application-oracle.properties`. +- Activated with `SPRING_PROFILES_ACTIVE=oracle`. +- Uses environment variables for database URL, username, and password. +- The dashboard works with Oracle as long as `/history` returns a successful JSON response. + +### Oracle XE With Docker + +Start Oracle XE in Docker: + +```powershell +docker run -d --name oracle-xe ` + -p 1521:1521 ` + -e ORACLE_PASSWORD=oracle ` + -e APP_USER=oraclequantapi ` + -e APP_USER_PASSWORD=oraclequantapi ` + -v oracle-xe-data:/opt/oracle/oradata ` + gvenzl/oracle-xe:21-slim +``` + +Wait until Oracle is ready: + +```powershell +docker logs -f oracle-xe +``` + +Wait for this message: + +```text +DATABASE IS READY TO USE! +``` + +Set Oracle profile variables on Windows PowerShell: + +```powershell +$env:SPRING_PROFILES_ACTIVE="oracle" +$env:ORACLE_DB_URL="jdbc:oracle:thin:@//localhost:1521/XEPDB1" +$env:ORACLE_DB_USERNAME="oraclequantapi" +$env:ORACLE_DB_PASSWORD="oraclequantapi" +``` + +Run the application: + +```powershell +.\mvnw.cmd spring-boot:run +``` + +Useful Docker commands: + +```powershell +docker ps +docker stop oracle-xe +docker start oracle-xe +``` + +### Oracle XE Without Docker + +If Oracle XE is installed directly on the machine, create the application user in Oracle: + +```sql +CREATE USER oraclequantapi IDENTIFIED BY "oraclequantapi"; +GRANT CREATE SESSION TO oraclequantapi; +GRANT CREATE TABLE TO oraclequantapi; +GRANT CREATE SEQUENCE TO oraclequantapi; +ALTER USER oraclequantapi QUOTA UNLIMITED ON USERS; +``` + +Use this JDBC URL for a common Oracle XE installation: + +```text +jdbc:oracle:thin:@//localhost:1521/XEPDB1 +``` + +Set Oracle profile variables on Linux: + +```bash +export SPRING_PROFILES_ACTIVE=oracle +export ORACLE_DB_URL='jdbc:oracle:thin:@//localhost:1521/XEPDB1' +export ORACLE_DB_USERNAME='oraclequantapi' +export ORACLE_DB_PASSWORD='oraclequantapi' +``` + +The application uses Hibernate `ddl-auto=update` by default for this assignment. Override it if needed: + +```bash +export ORACLE_DDL_AUTO=validate +``` + +If the dashboard shows a history warning, test the history endpoint directly: + +```text +http://localhost:8080/history +``` + +If `/history` returns `500`, verify that the Oracle user used by the app owns a compatible `CONVERSION_HISTORY` table. + +Expected table columns: + +```text +ID +CREATED_AT +SOURCE_IP_ADDRESS +INPUT +OUTPUT +``` + +If you do not need old history records, the simplest repair is to drop the old table and restart the app so Hibernate recreates it: + +```sql +DROP TABLE CONVERSION_HISTORY; +``` + +## REST API Endpoints + +### Convert Measurements + +Endpoint: + +```http +GET /convert-measurements?input={measurement-string} +``` + +Encoding rules: + +- `_` means `0`. +- `a` means `1`, `b` means `2`, through `z` meaning `26`. +- Values above `26` are encoded by adding characters together. +- A multi-character value starts with one or more `z` characters and ends at the first following non-`z` character. +- Each package starts with a count value, followed by that many measured values. +- Invalid characters are treated as `0`. +- Missing measured values are treated as `0`. +- Empty or missing `input` returns an empty list. + +Examples: + +```bash +curl 'http://localhost:8080/convert-measurements?input=aa' +# [1] + +curl 'http://localhost:8080/convert-measurements?input=abbcc' +# [2,6] + +curl 'http://localhost:8080/convert-measurements?input=dz_a_aazzaaa' +# [28,53,1] + +curl 'http://localhost:8080/convert-measurements?input=a_' +# [0] + +curl 'http://localhost:8080/convert-measurements?input=abcdabcdab' +# [2,7,7] + +curl 'http://localhost:8080/convert-measurements?input=abcdabcdab_' +# [2,7,7,0] + +curl 'http://localhost:8080/convert-measurements?input=caa' +# [2] + +curl 'http://localhost:8080/convert-measurements?input=a1' +# [0] +``` + +### History Endpoints + +History records include: + +- `id` +- `timestamp` +- `source_ip_address` +- `input` +- `output` + +Available endpoints: + +```http +GET /history +GET /history/{id} +PUT /history/{id} +PATCH /history/{id} +DELETE /history +DELETE /history/{id} +``` + +Get all history records: + +```bash +curl 'http://localhost:8080/history' +``` + +Get one history record: + +```bash +curl 'http://localhost:8080/history/1' +``` + +Patch one history record: + +```bash +curl -X PATCH 'http://localhost:8080/history/1' \ + -H 'Content-Type: application/json' \ + -d '{"input":"aa","output":"[1]","source_ip_address":"127.0.0.1"}' +``` + +Replace or update one history record: + +```bash +curl -X PUT 'http://localhost:8080/history/1' \ + -H 'Content-Type: application/json' \ + -d '{"timestamp":"2026-05-25T12:00:00Z","input":"abbcc","output":"[2,6]","source_ip_address":"127.0.0.1"}' +``` + +Delete one history record: + +```bash +curl -X DELETE 'http://localhost:8080/history/1' +``` + +Delete all history records: + +```bash +curl -X DELETE 'http://localhost:8080/history' +``` + +## Logging + +Logging is configured in: + +```text +src/main/resources/logback-spring.xml +``` + +The application writes logs to: + +- Console output +- `logs/oraclequantapi.log` + +Daily rolling log files are retained for seven days. + +Override the log path: + +```bash +export LOG_PATH=/var/log/oraclequantapi +``` + +## Deploy The Jar On Oracle Linux Via SSH + +Build the jar locally: + +```powershell +.\mvnw.cmd clean package +``` + +SSH to the Oracle Linux server: + +```bash +ssh opc@your-server-ip +``` + +Create the application directory on the server: + +```bash +sudo mkdir -p /opt/oraclequantapi +sudo chown opc:opc /opt/oraclequantapi +``` + +Exit the server and copy the jar from your local machine: + +```bash +scp target/oraclequantapi-0.0.1-SNAPSHOT.jar opc@your-server-ip:/opt/oraclequantapi/oraclequantapi.jar +``` + +SSH back to the server: + +```bash +ssh opc@your-server-ip +``` + +Install Oracle OpenJDK 17 if needed: + +```bash +sudo dnf install -y jdk-17 +java -version +``` + +Create a runtime environment file: + +```bash +sudo tee /etc/oraclequantapi.env >/dev/null <<'EOF' +SPRING_PROFILES_ACTIVE=oracle +ORACLE_DB_URL=jdbc:oracle:thin:@//localhost:1521/XEPDB1 +ORACLE_DB_USERNAME=oraclequantapi +ORACLE_DB_PASSWORD=oraclequantapi +LOG_PATH=/var/log/oraclequantapi +EOF +``` + +Create the log directory: + +```bash +sudo mkdir -p /var/log/oraclequantapi +sudo chown opc:opc /var/log/oraclequantapi +``` + +Run the jar manually: + +```bash +set -a +. /etc/oraclequantapi.env +set +a +java -jar /opt/oraclequantapi/oraclequantapi.jar +``` + +Test from another terminal or browser: + +```text +http://your-server-ip:8080/convert-measurements?input=abbcc +``` + +Optional systemd service file: + +```ini +[Unit] +Description=OracleQuant API +After=network.target + +[Service] +EnvironmentFile=/etc/oraclequantapi.env +ExecStart=/usr/bin/java -jar /opt/oraclequantapi/oraclequantapi.jar +Restart=always +User=opc + +[Install] +WantedBy=multi-user.target +``` + +Save the service file as: + +```text +/etc/systemd/system/oraclequantapi.service +``` + +Enable and start the service: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now oraclequantapi +sudo systemctl status oraclequantapi +``` + +View application logs: + +```bash +journalctl -u oraclequantapi -f +tail -f /var/log/oraclequantapi/oraclequantapi.log +``` + +## Project Files + +Main application class: + +```text +src/main/java/com/oraclequantapi/oraclequantapi/OracleQuantApiApplication.java +``` + +Main packages: + +```text +controller +service +model +repository +``` + +Important configuration files: + +```text +src/main/resources/application.properties +src/main/resources/application-oracle.properties +src/main/resources/logback-spring.xml +``` diff --git a/pom.xml b/pom.xml index 20909d2..1e5b7d1 100644 --- a/pom.xml +++ b/pom.xml @@ -11,8 +11,8 @@ com.oraclequantapi oraclequantapi 0.0.1-SNAPSHOT - - + oraclequantapi + OracleQuant package measurement conversion API @@ -34,6 +34,20 @@ org.springframework.boot spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.oracle.database.jdbc + ojdbc11 + runtime + + + com.h2database + h2 + runtime + org.springframework.boot diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controller/HistoryController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controller/HistoryController.java new file mode 100644 index 0000000..4760b99 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controller/HistoryController.java @@ -0,0 +1,125 @@ +package com.oraclequantapi.oraclequantapi.controller; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import com.oraclequantapi.oraclequantapi.service.HistoryService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@RestController // Marks this class as a REST controller +@RequestMapping("/history") // Base URL path for all endpoints in this controller +public class HistoryController { + + // Logger used for tracking API requests and actions + private static final Logger log = LoggerFactory.getLogger(HistoryController.class); + + // Service layer dependency + private final HistoryService historyService; + + // Constructor dependency injection + public HistoryController(HistoryService historyService) { + this.historyService = historyService; + } + + // GET endpoint to return all history records + @GetMapping + public List findAll() { + log.info("Fetching all history records"); + return historyService.findAll(); + } + + // GET endpoint to return history record by ID + @GetMapping("/{id}") + public HistoryRecord findById(@PathVariable Long id) { + log.info("Fetching history record with id {}", id); + return historyService.findById(id); + } + + // PUT endpoint for full update + @PutMapping("/{id}") + public HistoryRecord put( + @PathVariable Long id, + @RequestBody HistoryUpdateRequest request + ) { + log.info("PUT update request received for history record {}", id); + return update(id, request); + } + + // PATCH endpoint for partial update + @PatchMapping("/{id}") + public HistoryRecord patch( + @PathVariable Long id, + @RequestBody HistoryUpdateRequest request + ) { + log.info("PATCH update request received for history record {}", id); + return update(id, request); + } + + // DELETE endpoint to remove all history records + @DeleteMapping + public ResponseEntity deleteAll() { + log.warn("Deleting all history records"); + historyService.deleteAll(); + log.info("All history records deleted successfully"); + return ResponseEntity.noContent().build(); + } + + // DELETE endpoint to remove a record by ID + @DeleteMapping("/{id}") + public ResponseEntity deleteById(@PathVariable Long id) { + log.warn("Deleting history record with id {}", id); + historyService.deleteById(id); + log.info("History record {} deleted successfully", id); + return ResponseEntity.noContent().build(); + } + + // Handles custom not found exceptions + @ExceptionHandler(HistoryService.HistoryRecordNotFoundException.class) + public ResponseEntity> historyNotFound( + HistoryService.HistoryRecordNotFoundException exception + ) { + log.warn("History record not found: {}", exception.getMessage()); + return error(HttpStatus.NOT_FOUND, exception.getMessage()); + } + + // Shared method for updating records + private HistoryRecord update(Long id, HistoryUpdateRequest request) { + HistoryRecord record = historyService.update( + id, + request.timestamp(), + request.sourceIpAddress(), + request.input(), + request.output() + ); + + log.info("History record {} updated successfully", id); + return record; + } + + // Builds formatted error responses + private ResponseEntity> error(HttpStatus status, String message) { + return ResponseEntity.status(status).body(Map.of( + "timestamp", Instant.now().toString(), + "status", status.value(), + "error", status.getReasonPhrase(), + "message", message + )); + } + + // Request body model for updates + public record HistoryUpdateRequest( + LocalDateTime timestamp, + @JsonProperty("source_ip_address") + @JsonAlias("sourceIpAddress") + String sourceIpAddress, String input, String output) { + } +} \ No newline at end of file diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controller/MeasurementController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controller/MeasurementController.java new file mode 100644 index 0000000..03ad6ba --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controller/MeasurementController.java @@ -0,0 +1,70 @@ +package com.oraclequantapi.oraclequantapi.controller; + +import com.oraclequantapi.oraclequantapi.service.HistoryService; +import com.oraclequantapi.oraclequantapi.service.MeasurementService; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 // Marks this class as a REST controller +public class MeasurementController { + // Logger used for request and response logging + private static final Logger log = LoggerFactory.getLogger(MeasurementController.class); + + // Service responsible for conversion logic + private final MeasurementService measurementService; + + // Service responsible for history storage + private final HistoryService historyService; + + // Constructor dependency injection + public MeasurementController(MeasurementService measurementService, HistoryService historyService) { + this.measurementService = measurementService; + this.historyService = historyService; + } + + // HTTP GET endpoint for measurement conversion + @GetMapping("/convert-measurements") + public List convert(@RequestParam(required = false) String input, HttpServletRequest request) { + + // Prevent null input values + String safeInput; + if (input == null) { + safeInput = ""; + } + else { + safeInput = input; + } + + // Log received input + log.info("Received input: {}", safeInput); + + // Convert encoded input into totals + List output = measurementService.convert(safeInput); + + // Log conversion output + log.info("Conversion result: {}", output); + + // Save request history into database + historyService.record(safeInput, output, clientIp(request)); + + // Log client IP address + log.info("Request saved from IP: {}", clientIp(request)); + + return output; // Return conversion result + } + + // Extract client IP address from request + private String clientIp(HttpServletRequest request) { + String forwardedFor = request.getHeader("X-Forwarded-For"); + if (forwardedFor != null && !forwardedFor.isBlank()) { + return forwardedFor.split(",")[0].trim(); + } + return request.getRemoteAddr(); // Fallback to direct remote address + } +} \ No newline at end of file diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/model/HistoryRecord.java b/src/main/java/com/oraclequantapi/oraclequantapi/model/HistoryRecord.java new file mode 100644 index 0000000..546ddb1 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/model/HistoryRecord.java @@ -0,0 +1,99 @@ +package com.oraclequantapi.oraclequantapi.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; + +import jakarta.persistence.*; + +import java.time.LocalDateTime; + +@Entity // Marks this class as a database entity +@Table(name = "CONVERSION_HISTORY") // Maps this entity to the CONVERSION_HISTORY table +public class HistoryRecord { + + @Id // Primary key of the table + @GeneratedValue(strategy = GenerationType.IDENTITY) // Auto generate ID values + @Column(name = "ID") // Maps the field to the ID column + private Long id; + + @Column(name = "CREATED_AT", nullable = false) // Stores the record creation timestamp + private LocalDateTime timestamp; + + @Column(name = "SOURCE_IP_ADDRESS", nullable = false, length = 128) // Stores the source IP address + @JsonProperty("source_ip_address") // JSON property name when sending response + @JsonAlias("sourceIpAddress") // Accept alternative JSON field name + private String sourceIpAddress; + + // Stores request input text + @Column(name = "INPUT", length = 4000) + private String input; + + // Stores generated output JSON + @Column(name = "OUTPUT", nullable = false, length = 4000) + private String output; + + // Default constructor required by JPA + protected HistoryRecord() { + } + + // Constructor used to create a new history record + public HistoryRecord(String sourceIpAddress, String input, String output) { + this.sourceIpAddress = sourceIpAddress; + this.input = input; + this.output = output; + } + + // Automatically runs before saving the entity + @PrePersist + void onCreate() { + // Set current timestamp if it is empty + if (timestamp == null) { + timestamp = LocalDateTime.now(); + } + } + + // Returns the database ID + public Long getId() { + return id; + } + + // Returns the creation timestamp + public LocalDateTime getTimestamp() { + return timestamp; + } + + // Updates the timestamp + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + // Returns the source IP address + public String getSourceIpAddress() { + return sourceIpAddress; + } + + // Updates the source IP address + public void setSourceIpAddress(String sourceIpAddress) { + this.sourceIpAddress = sourceIpAddress; + } + + // Returns the input value + public String getInput() { + return input; + } + + // Updates the input value + public void setInput(String input) { + this.input = input; + } + + // Returns the output value + public String getOutput() { + return output; + } + + // Updates the output value + public void setOutput(String output) { + this.output = output; + } +} \ No newline at end of file diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/repository/HistoryRepository.java b/src/main/java/com/oraclequantapi/oraclequantapi/repository/HistoryRepository.java new file mode 100644 index 0000000..ca8e43f --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/repository/HistoryRepository.java @@ -0,0 +1,8 @@ +package com.oraclequantapi.oraclequantapi.repository; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import org.springframework.data.jpa.repository.JpaRepository; + +// Repository interface used for database operations +public interface HistoryRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/service/HistoryService.java b/src/main/java/com/oraclequantapi/oraclequantapi/service/HistoryService.java new file mode 100644 index 0000000..7a88c3f --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/service/HistoryService.java @@ -0,0 +1,102 @@ +package com.oraclequantapi.oraclequantapi.service; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import com.oraclequantapi.oraclequantapi.repository.HistoryRepository; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service // Marks this class as a Spring service +public class HistoryService { + + private final HistoryRepository repository; // Repository used for database operations + private final ObjectMapper objectMapper; // ObjectMapper used to convert Java objects into JSON + + // Constructor dependency injection + public HistoryService(HistoryRepository repository, ObjectMapper objectMapper) { + this.repository = repository; + this.objectMapper = objectMapper; + } + + // Saves conversion history into the database + @Transactional + public HistoryRecord record(String input, List output, String sourceIpAddress) { + try { + String outputJson = objectMapper.writeValueAsString(output); + return repository.save(new HistoryRecord(sourceIpAddress, input, outputJson)); + } catch (JsonProcessingException exception) { + throw new IllegalStateException("Unable to serialize conversion output", exception); + } + } + + // Returns all history records sorted by ID + @Transactional(readOnly = true) + public List findAll() { + return repository.findAll(Sort.by(Sort.Direction.ASC, "id")); + } + + // Finds history record using ID + @Transactional(readOnly = true) + public HistoryRecord findById(Long id) { + return repository.findById(id) + .orElseThrow(() -> new HistoryRecordNotFoundException(id)); + } + + // Updates existing history record + @Transactional + public HistoryRecord update( + Long id, + LocalDateTime timestamp, + String sourceIpAddress, + String input, + String output + ) { + HistoryRecord record = findById(id); + + if (timestamp != null) { + record.setTimestamp(timestamp); + } + + if (sourceIpAddress != null) { + record.setSourceIpAddress(sourceIpAddress); + } + + if (input != null) { + record.setInput(input); + } + + if (output != null) { + record.setOutput(output); + } + + return repository.save(record); + } + + // Deletes all history records + @Transactional + public void deleteAll() { + repository.deleteAllInBatch(); + } + + // Deletes history record using ID + @Transactional + public void deleteById(Long id) { + if (!repository.existsById(id)) { + throw new HistoryRecordNotFoundException(id); + } + + repository.deleteById(id); + } + + // Custom exception for missing history records + public static class HistoryRecordNotFoundException extends RuntimeException { + public HistoryRecordNotFoundException(Long id) { + super("History record not found: " + id); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/service/MeasurementService.java b/src/main/java/com/oraclequantapi/oraclequantapi/service/MeasurementService.java new file mode 100644 index 0000000..f5eefe9 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/service/MeasurementService.java @@ -0,0 +1,90 @@ +package com.oraclequantapi.oraclequantapi.service; + +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + +@Service // Marks this class as a Spring service component +public class MeasurementService { + + // Converts encoded input into list of totals + public List convert(String input) { + + // Return empty list if input is null + if (input == null) { + return List.of(); + } + + List totals = new ArrayList<>(); // Store calculated totals + int index = 0; // Current reading position in the input string + + // Continue while there are remaining characters + while (index < input.length()) { + ParsedNumber count = readNumber(input, index); // Read how many measurements should be processed + index = count.nextIndex(); // Move to the next unread position + int total = 0; // Store total for current group + + // Loop through measurements + for (int valueIndex = 0; valueIndex < count.value(); valueIndex++) { + // Skip if end of string is reached + if (index >= input.length()) { + continue; + } + + ParsedNumber measurement = readNumber(input, index); // Read next measurement value + total += measurement.value(); // Add measurement value to total + index = measurement.nextIndex(); // Move to next unread position + } + totals.add(total); // Save total into result list + } + return totals; // Return all totals + } + + // Reads encoded numbers from the input string + private ParsedNumber readNumber(String input, int startIndex) { + char first = input.charAt(startIndex); // Get current character + + // Single character number handling + if (first != 'z') { + return new ParsedNumber(symbolValue(first), startIndex + 1); + } + + // Start reading z-based values + int index = startIndex; + int value = 0; + + // Every z adds 26 + while (index < input.length() && input.charAt(index) == 'z') { + value += 26; + index++; + } + + // Read final terminating character + if (index < input.length()) { + char terminator = input.charAt(index); + value += symbolValue(terminator); // Add remaining value + index++; // Move to next character + } + return new ParsedNumber(value, index); // Return parsed value and next position + } + + // Converts alphabet symbols into numeric values + private int symbolValue(char symbol) { + + // Underscore means zero + if (symbol == '_') { + return 0; + } + + // Reject invalid characters + if (symbol < 'a' || symbol > 'z') { + return 0; + } + return symbol - 'a' + 1; // Convert a-z into 1-26 + } + + // Stores parsed number value and next index + private record ParsedNumber(int value, int nextIndex) { + } +} \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..291ea44 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,26 @@ + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + ${LOG_PATH}/oraclequantapi.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + ${LOG_PATH}/oraclequantapi.%d{yyyy-MM-dd}.log + 7 + + + + + + + + diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..64dbdb9 --- /dev/null +++ b/version.txt @@ -0,0 +1,58 @@ +Current Documentation Version: 0.0.11-SNAPSHOT +Maven Artifact Version: 0.0.1-SNAPSHOT +Last Updated: 2026-05-25 + +Version History: + +0.0.7-SNAPSHOT - 2026-05-25 +- application-oracle.properties documentation updated +- Oracle XE profile configuration documented +- SPRING_PROFILES_ACTIVE=oracle usage documented +- logback-spring.xml documentation updated +- Console logging, rolling file logging, and seven-day log retention documented +- Default log file path documented as logs/oraclequantapi.log + +0.0.6-SNAPSHOT - 2026-05-23 +- Logging documentation updated +- Logback console logging documented +- Logback rolling file logging documented +- Default log file path documented as logs/oraclequantapi.log +- Seven-day rolling log retention documented + +0.0.5-SNAPSHOT - 2026-05-22 +- Project reviewed after database model updates +- ConversionHistory entity added +- ConversionHistoryRepository added +- Active history API still uses HistoryRecord, HistoryRepository, and HistoryService +- CHANGELOG.md and version.txt updated + +0.0.4-SNAPSHOT - 2026-05-22 +- Invalid characters count as 0 +- Missing measured values count as 0 +- Empty or missing input returns an empty list +- History persistence supports empty request input values +- Tests updated for tolerant conversion behavior + +0.0.3-SNAPSHOT - 2026-05-21 +- Project refactored into controller, service, model, and repository packages +- Main application class renamed to OracleQuantApiApplication +- Services and repositories renamed to match the requested structure +- Tests updated for the new structure + +0.0.2-SNAPSHOT - 2026-05-21 +- Conversion REST API added +- Measurement conversion algorithm added +- Request history persistence added +- History REST endpoints added +- Oracle XE database profile added +- Local H2 development database added +- Logback console logging added +- Logback daily rolling file logging added +- Default log file path added as logs/oraclequantapi.log +- Seven-day rolling log retention added +- Unit and integration tests added + +0.0.1-SNAPSHOT - 2026-05-21 +- Initial Spring Boot Maven project created +- Basic application entry point added +- Initial README added