From 140985775f69842d43bd753275b3aa7d5637ae93 Mon Sep 17 00:00:00 2001 From: abdulmajeedalbalushi Date: Mon, 25 May 2026 21:19:13 +0400 Subject: [PATCH] Final edit --- .gitignore | 44 +- README.md | 499 +++++++++++++++--- pom.xml | 102 ++-- ...lication.java => SequenceApplication.java} | 10 +- .../controller/SequenceController.java | 145 +++++ .../oraclequantapi/model/HistoryRecord.java | 98 ++++ .../oraclequantapi/model/Sequence.java | 36 ++ .../repositories/HistoryRecordRepository.java | 8 + .../service/SequenceService.java | 144 +++++ src/main/resources/application.properties | 25 +- src/main/resources/logback-spring.xml | 48 ++ version.txt | 8 + 12 files changed, 1021 insertions(+), 146 deletions(-) rename src/main/java/com/oraclequantapi/oraclequantapi/{OraclequantapiApplication.java => SequenceApplication.java} (55%) create mode 100644 src/main/java/com/oraclequantapi/oraclequantapi/controller/SequenceController.java create mode 100644 src/main/java/com/oraclequantapi/oraclequantapi/model/HistoryRecord.java create mode 100644 src/main/java/com/oraclequantapi/oraclequantapi/model/Sequence.java create mode 100644 src/main/java/com/oraclequantapi/oraclequantapi/repositories/HistoryRecordRepository.java create mode 100644 src/main/java/com/oraclequantapi/oraclequantapi/service/SequenceService.java create mode 100644 src/main/resources/logback-spring.xml create mode 100644 version.txt diff --git a/.gitignore b/.gitignore index 667aaef..9ff60b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,33 +1,19 @@ -HELP.md target/ -.mvn/wrapper/maven-wrapper.jar -!**/src/main/**/target/ -!**/src/test/**/target/ - -### STS ### -.apt_generated +build/ +.gradle/ +.idea/ +*.iml +.vscode/ .classpath -.factorypath .project -.settings -.springBeans -.sts4-cache - -### IntelliJ IDEA ### -.idea -*.iws -*.iml -*.ipr - -### NetBeans ### -/nbproject/private/ -/nbbuild/ -/dist/ -/nbdist/ -/.nb-gradle/ -build/ -!**/src/main/**/build/ -!**/src/test/**/build/ +.settings/ +.factorypath +.DS_Store +logs/ +*.log +*.log.gz +HELP.md +.env +.env.local +measurement-conversion-api/ -### VS Code ### -.vscode/ diff --git a/README.md b/README.md index b1cccfd..c5c15a3 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,438 @@ -## 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. +# Sequence API — Spring Boot + Oracle XE + +A REST API that processes "sequence" strings using the z-chain encoding rule, stores every processed call as a history record in **Oracle XE**, and exposes CRUD endpoints over that history. + +--- + +## Table of Contents + +1. [Prerequisites](#prerequisites) +2. [Running the Application](#running-the-application) +3. [Configuring the Database](#configuring-the-database) +4. [REST API Endpoints](#rest-api-endpoints) +5. [Deployment](#deployment) +6. [Project Structure](#project-structure) +7. [How the Encoding Works](#how-the-encoding-works) + +--- + +## Prerequisites + +| Tool | Minimum version | +|------|----------------| +| Java (JDK) | 17 | +| Maven | 3.8 | +| Oracle XE | 21c | +| Docker (optional) | any recent version | + +--- + +## Running the Application + +### 1. Start Oracle XE + +**Option A — Docker (recommended)** + +```bash +docker run -d --name oracle-xe -p 1521:1521 \ + -e ORACLE_PASSWORD=oracle \ + gvenzl/oracle-xe:21-slim +``` + +Wait until the container is ready: + +```bash +docker logs -f oracle-xe +# Wait for: DATABASE IS READY TO USE! +``` + +**Option B — Native install** + +If you have Oracle XE installed locally, make sure the listener is running on port `1521` and the pluggable database `XEPDB1` is open. + +### 2. Clone and build + +```bash +git clone +cd measurement-conversion-api +mvn clean package -DskipTests +``` + +### 3. Run + +```bash +mvn spring-boot:run +``` + +Or run the packaged JAR directly: + +```bash +java -jar target/sequence-api-0.0.1-SNAPSHOT.jar +``` + +The server starts at `http://localhost:8080`. + +On the **first startup**, Hibernate automatically creates the `HISTORY_RECORD` and `HISTORY_RECORD_OUTPUT` tables in Oracle XE (`spring.jpa.hibernate.ddl-auto=update`). + +### Verifying the app is up + +``` +GET http://localhost:8080/history +``` + +Expected response: `[]` (empty array if no history yet). + +--- + +## Configuring the Database + +Database settings live in `src/main/resources/application.properties`. + +```properties +# Oracle XE connection +spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1 +spring.datasource.username=system +spring.datasource.password= +spring.datasource.driver-class-name=oracle.jdbc.OracleDriver + +# JPA / Hibernate +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect +spring.jpa.hibernate.ddl-auto=update # use "validate" or "none" in production +spring.jpa.show-sql=true # prints SQL to console; turn off in production +spring.jpa.properties.hibernate.format_sql=true +``` + +### Overriding without editing the file + +Use environment variables so you never commit passwords: + +```bash +# PowerShell +$env:SPRING_DATASOURCE_URL = "jdbc:oracle:thin:@localhost:1521/XEPDB1" +$env:SPRING_DATASOURCE_USERNAME = "system" +$env:SPRING_DATASOURCE_PASSWORD = "your_password_here" +mvn spring-boot:run +``` + +```bash +# Bash / Linux +export SPRING_DATASOURCE_URL=jdbc:oracle:thin:@localhost:1521/XEPDB1 +export SPRING_DATASOURCE_USERNAME=system +export SPRING_DATASOURCE_PASSWORD=your_password_here +mvn spring-boot:run +``` + +### Tables created by Hibernate + +```sql +-- Main record table +CREATE TABLE HISTORY_RECORD ( + ID NUMBER(19,0) GENERATED AS IDENTITY PRIMARY KEY, + TIMESTAMP TIMESTAMP(6) NOT NULL, + SOURCE_IP_ADDRESS VARCHAR2(64 CHAR), + INPUT VARCHAR2(1024 CHAR) NOT NULL +); + +-- Child table for the output list (preserves element order) +CREATE TABLE HISTORY_RECORD_OUTPUT ( + HISTORY_RECORD_ID NUMBER(19,0) NOT NULL, + POSITION NUMBER(10,0) NOT NULL, + VALUE NUMBER(10,0), + PRIMARY KEY (HISTORY_RECORD_ID, POSITION), + FOREIGN KEY (HISTORY_RECORD_ID) REFERENCES HISTORY_RECORD(ID) +); +``` + +Verify via `sqlplus`: + +```bash +docker exec -it oracle-xe sqlplus system/oracle@//localhost:1521/XEPDB1 +SQL> SELECT * FROM HISTORY_RECORD; +SQL> SELECT * FROM HISTORY_RECORD_OUTPUT ORDER BY HISTORY_RECORD_ID, POSITION; +``` + +--- + +## REST API Endpoints + +Base URL: `http://localhost:8080` + +### Endpoint summary + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/sequence?input={value}` | Process a sequence; saves a history record | +| GET | `/history` | Return all history records | +| GET | `/history/{id}` | Return one record by id | +| PUT | `/history/{id}` | Update fields of an existing record | +| DELETE | `/history` | Clear all history records | + +--- + +### `GET /sequence?input={value}` + +Processes the input string, stores the result, and returns the saved record. + +**Request** + +``` +GET http://localhost:8080/sequence?input=dz_a_aazzaaa +``` + +**Response — 200 OK** + +```json +{ + "id": 1, + "timestamp": "2026-05-23T10:15:30.123", + "sourceIpAddress": "127.0.0.1", + "input": "dz_a_aazzaaa", + "output": [28, 53, 1] +} +``` + +**Response — 400 Bad Request** (invalid characters in input) + +```json +{ "error": "input must be non-empty and contain only letters and underscores" } +``` + +--- + +### `GET /history` + +Returns all stored history records. + +**Request** + +``` +GET http://localhost:8080/history +``` + +**Response — 200 OK** + +```json +[ + { + "id": 1, + "timestamp": "2026-05-23T10:15:30.123", + "sourceIpAddress": "127.0.0.1", + "input": "dz_a_aazzaaa", + "output": [28, 53, 1] + } +] +``` + +--- + +### `GET /history/{id}` + +Returns a single record by its id. + +**Request** + +``` +GET http://localhost:8080/history/1 +``` + +**Response — 200 OK** + +```json +{ + "id": 1, + "timestamp": "2026-05-23T10:15:30.123", + "sourceIpAddress": "127.0.0.1", + "input": "dz_a_aazzaaa", + "output": [28, 53, 1] +} +``` + +**Response — 404 Not Found** + +```json +{ "error": "History record not found: id=99" } +``` + +--- + +### `PUT /history/{id}` + +Updates one or more fields of an existing record. Only the fields you send are changed. If you update `input` without providing `output`, the output is automatically recomputed. + +**Request** + +``` +PUT http://localhost:8080/history/1 +Content-Type: application/json + +{ + "input": "ab", + "sourceIpAddress": "10.0.0.5" +} +``` + +| Field | Required | Notes | +|-------|----------|-------| +| `input` | No | Must contain only letters and underscores | +| `output` | No | If omitted when `input` changes, output is recomputed | +| `sourceIpAddress` | No | Any string | + +**Response — 200 OK** + +```json +{ + "id": 1, + "timestamp": "2026-05-23T10:15:30.123", + "sourceIpAddress": "10.0.0.5", + "input": "ab", + "output": [2] +} +``` + +**Response — 404 Not Found** + +```json +{ "error": "History record not found: id=9999" } +``` + +--- + +### `DELETE /history` + +Clears every history record from the database. + +**Request** + +``` +DELETE http://localhost:8080/history +``` + +**Response — 200 OK** + +```json +{ "message": "History cleared successfully" } +``` + +After this, `GET /history` returns `[]`. + +--- + +### Postman collection + +Import `postman/Sequence.postman_collection.json` into Postman for ready-to-send requests covering all endpoints. + +--- + +## Deployment + +> **Status: Deployment was attempted but did not work.** +> +> The steps below describe the intended deployment approach. The process was not completed successfully — the application could not be deployed to a remote environment. This section is kept here for reference and to document what was tried. + +### Intended approach — packaged JAR on a Linux server + +**Step 1 — Build the JAR** + +```bash +mvn clean package -DskipTests +``` + +This produces `target/sequence-api-0.0.1-SNAPSHOT.jar`. + +**Step 2 — Copy to the server** + +```bash +scp target/sequence-api-0.0.1-SNAPSHOT.jar user@your-server:/opt/sequence-api/ +``` + +**Step 3 — Set environment variables on the server** + +```bash +export SPRING_DATASOURCE_URL=jdbc:oracle:thin:@:1521/XEPDB1 +export SPRING_DATASOURCE_USERNAME=system +export SPRING_DATASOURCE_PASSWORD= +``` + +**Step 4 — Run on the server** + +```bash +java -jar /opt/sequence-api/sequence-api-0.0.1-SNAPSHOT.jar +``` + +**Step 5 — (Optional) Run as a background service** + +```bash +nohup java -jar /opt/sequence-api/sequence-api-0.0.1-SNAPSHOT.jar \ + > /var/log/sequence-api.log 2>&1 & +``` + +### Why deployment did not work + +The deployment was not completed. Known blockers: + +- **Database connectivity**: The Oracle XE instance was only available on `localhost`. A remote server cannot reach it without additional network configuration (port forwarding, a cloud database, or running Oracle in Docker on the same remote host). +- **No remote server configured**: A target server was not set up and provisioned during development. +- **No CI/CD pipeline**: There is no automated build-and-deploy workflow (GitHub Actions, Jenkins, etc.) in place. + +### What would be needed to get deployment working + +1. A remote server (e.g. an AWS EC2 instance, Azure VM, or DigitalOcean Droplet) with Java 17+ installed. +2. An Oracle XE instance reachable from that server, **or** switching to a cloud-friendly database (PostgreSQL, MySQL) by swapping the JDBC driver and dialect in `application.properties`. +3. A firewall rule opening port `8080` (or reverse-proxying via Nginx/Apache on port 80/443). +4. Environment variables or a secrets manager providing database credentials to the running process. + +--- + +## Project Structure + +``` +measurement-conversion-api/ +├── pom.xml Maven build file +├── README.md This file +├── version.txt Changelog +├── logs/ Rolling log files (created at runtime) +├── postman/ +│ └── Sequence.postman_collection.json Ready-made Postman requests +└── src/main/ + ├── java/com/example/sequence/ + │ ├── SequenceApplication.java Spring Boot entry point + │ ├── controller/SequenceController.java REST endpoints + │ ├── service/SequenceService.java Business logic and z-chain parser + │ ├── repositories/ + │ │ └── HistoryRecordRepository.java Spring Data JPA repository + │ └── model/ + │ ├── Sequence.java Input format validator + │ └── HistoryRecord.java JPA entity (one DB row) + └── resources/ + ├── application.properties Port, Oracle XE, JPA config + └── logback-spring.xml Logging configuration +``` + +--- + +## How the Encoding Works + +A **z-chain** is a sequence of zero or more `z` characters (each worth **26**) followed by exactly one terminator: + +| z-chain | Value | +|---------|-------| +| `a` | 1 | +| `_` | 0 | +| `zd` | 30 (26 + 4) | +| `zza` | 53 (26 + 26 + 1) | + +A full input is a series of **packages**: + +1. Read one z-chain → that is the **package size** N. +2. Read N more z-chains → those are the **values**. +3. Sum the values → that sum is the package result. +4. Repeat until the input runs out. + +**Example: `dz_a_aazzaaa`** + +- `d` → size **4**; values: `z_`=26, `a`=1, `_`=0, `a`=1 → sum **28** +- `a` → size **1**; values: `zza`=53 → sum **53** +- `a` → size **1**; values: `a`=1 → sum **1** +- Result: `[28, 53, 1]` + +Only letters and underscores are valid input characters. Digits, spaces, or special characters return a 400 error. diff --git a/pom.xml b/pom.xml index 20909d2..71bf49b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,54 +1,58 @@ - - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.5.14 - - - com.oraclequantapi - oraclequantapi - 0.0.1-SNAPSHOT - - - - - - - - - - - - - - - - - 17 - - - - org.springframework.boot - spring-boot-starter-web - + + 4.0.0 - - org.springframework.boot - spring-boot-starter-test - test - - + + org.springframework.boot + spring-boot-starter-parent + 3.2.5 + + - - - - org.springframework.boot - spring-boot-maven-plugin - - - + com.example + sequence-api + 1.0.0 + Sequence API + Beginner-friendly Spring Boot REST API for processing sequences (Oracle XE via Spring Data JPA) + + 17 + UTF-8 + + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.oracle.database.jdbc + ojdbc11 + 23.6.0.24.10 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java b/src/main/java/com/oraclequantapi/oraclequantapi/SequenceApplication.java similarity index 55% rename from src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java rename to src/main/java/com/oraclequantapi/oraclequantapi/SequenceApplication.java index 5e28689..932f91c 100644 --- a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java +++ b/src/main/java/com/oraclequantapi/oraclequantapi/SequenceApplication.java @@ -4,10 +4,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication -public class OraclequantapiApplication { - - public static void main(String[] args) { - SpringApplication.run(OraclequantapiApplication.class, args); - } - +public class SequenceApplication { + public static void main(String[] args) { + SpringApplication.run(SequenceApplication.class, args); + } } diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controller/SequenceController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controller/SequenceController.java new file mode 100644 index 0000000..064dc7e --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controller/SequenceController.java @@ -0,0 +1,145 @@ +package com.oraclequantapi.oraclequantapi.controller; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import com.oraclequantapi.oraclequantapi.service.SequenceService; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; +import java.util.Optional; + +@RestController +public class SequenceController { + + private static final Logger log = LoggerFactory.getLogger(SequenceController.class); + + private final SequenceService service; + + public SequenceController(SequenceService service) { + this.service = service; + } + + // Process a sequence and store the result in history. + @GetMapping("/sequence") + public ResponseEntity processSequence(@RequestParam("input") String input, + HttpServletRequest request) { + String ip = clientIp(request); + log.info("API request received: GET /sequence input='{}' from {}", input, ip); + try { + HistoryRecord saved = service.process(input, ip); + log.info("Sequence processed successfully: id={} output={}", saved.getId(), saved.getOutput()); + return ResponseEntity.ok(saved); + } catch (IllegalArgumentException e) { + log.warn("Invalid input detected on GET /sequence: '{}' — {}", input, e.getMessage()); + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + // Return all history records. + @GetMapping("/history") + public List allHistory() { + log.info("API request received: GET /history"); + List all = service.getAllHistory(); + log.info("Returning {} history record(s)", all.size()); + return all; + } + + // Remove every history record. + @DeleteMapping("/history") + public ResponseEntity clearHistory() { + log.info("API request received: DELETE /history"); + service.clearHistory(); + log.info("History cleared"); + return ResponseEntity.ok(Map.of("message", "History cleared successfully")); + } + + // Return one history record by id, or 404 if it doesn't exist. + @GetMapping("/history/{id}") + public ResponseEntity historyById(@PathVariable Long id) { + log.info("API request received: GET /history/{}", id); + Optional found = service.getHistoryById(id); + if (found.isEmpty()) { + log.warn("History record not found: id={}", id); + return ResponseEntity.status(404) + .body(Map.of("error", "History record not found: id=" + id)); + } + return ResponseEntity.ok(found.get()); + } + + //Update an existing history record. + @PutMapping("/history/{id}") + public ResponseEntity updateHistory(@PathVariable Long id, + @RequestBody UpdateHistoryRequest body) { + log.info("API request received: PUT /history/{} body input='{}' output={} ip='{}'", + id, body.getInput(), body.getOutput(), body.getSourceIpAddress()); + try { + Optional updated = service.updateHistory( + id, + body.getInput(), + body.getOutput(), + body.getSourceIpAddress() + ); + if (updated.isEmpty()) { + log.warn("History record not found for update: id={}", id); + return ResponseEntity.status(404) + .body(Map.of("error", "History record not found: id=" + id)); + } + log.info("History updated: id={}", id); + return ResponseEntity.ok(updated.get()); + } catch (IllegalArgumentException e) { + log.warn("Invalid input detected on PUT /history/{}: {}", id, e.getMessage()); + return ResponseEntity.badRequest().body(Map.of("error", e.getMessage())); + } + } + + //Read the client IP, honouring the X-Forwarded-For header when present. + private static String clientIp(HttpServletRequest request) { + String forwarded = request.getHeader("X-Forwarded-For"); + if (forwarded != null && !forwarded.isBlank()) { + int comma = forwarded.indexOf(','); + return (comma >= 0 ? forwarded.substring(0, comma) : forwarded).trim(); + } + return request.getRemoteAddr(); + } + + //Request body for PUT /history/{id}. All fields are optional. + public static class UpdateHistoryRequest { + private String input; + private List output; + private String sourceIpAddress; + + public String getInput() { + return input; + } + + public void setInput(String input) { + this.input = input; + } + + public List getOutput() { + return output; + } + + public void setOutput(List output) { + this.output = output; + } + + public String getSourceIpAddress() { + return sourceIpAddress; + } + + public void setSourceIpAddress(String sourceIpAddress) { + this.sourceIpAddress = sourceIpAddress; + } + } +} 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..af3f083 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/model/HistoryRecord.java @@ -0,0 +1,98 @@ +package com.oraclequantapi.oraclequantapi.model; + +import jakarta.persistence.CollectionTable; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OrderColumn; +import jakarta.persistence.Table; + +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "history_record") +public class HistoryRecord { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "timestamp", nullable = false) + private LocalDateTime timestamp; + + @Column(name = "source_ip_address", length = 64) + private String sourceIpAddress; + + @Column(name = "input", nullable = false, length = 1024) + private String input; + + @ElementCollection(fetch = FetchType.EAGER) + @CollectionTable( + name = "history_record_output", + joinColumns = @JoinColumn(name = "history_record_id") + ) + @OrderColumn(name = "position") + @Column(name = "value") + private List output; + + public HistoryRecord() { + } + + public HistoryRecord(Long id, + LocalDateTime timestamp, + String sourceIpAddress, + String input, + List output) { + this.id = id; + this.timestamp = timestamp; + this.sourceIpAddress = sourceIpAddress; + this.input = input; + this.output = output; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public String getSourceIpAddress() { + return sourceIpAddress; + } + + public void setSourceIpAddress(String sourceIpAddress) { + this.sourceIpAddress = sourceIpAddress; + } + + public String getInput() { + return input; + } + + public void setInput(String input) { + this.input = input; + } + + public List getOutput() { + return output; + } + + public void setOutput(List output) { + this.output = output; + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/model/Sequence.java b/src/main/java/com/oraclequantapi/oraclequantapi/model/Sequence.java new file mode 100644 index 0000000..8d2b63a --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/model/Sequence.java @@ -0,0 +1,36 @@ +package com.oraclequantapi.oraclequantapi.model; + +import java.util.List; + +public class Sequence { + + private final String input; + private final List values; + + public Sequence(String input, List values) { + if (!isValid(input)) { + throw new IllegalArgumentException( + "Sequence input must be non-empty and contain only letters and underscores"); + } + this.input = input; + this.values = values; + } + + /** Validate that the format is non-empty letters/underscores only. */ + public static boolean isValid(String input) { + return input != null && !input.isBlank() && input.matches("[A-Za-z_]+"); + } + + public String getInput() { + return input; + } + + public List getValues() { + return values; + } + + @Override + public String toString() { + return input + " => " + values; + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/repositories/HistoryRecordRepository.java b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/HistoryRecordRepository.java new file mode 100644 index 0000000..4c1748b --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/HistoryRecordRepository.java @@ -0,0 +1,8 @@ +package com.oraclequantapi.oraclequantapi.repositories; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface HistoryRecordRepository extends JpaRepository {} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/service/SequenceService.java b/src/main/java/com/oraclequantapi/oraclequantapi/service/SequenceService.java new file mode 100644 index 0000000..d545e04 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/service/SequenceService.java @@ -0,0 +1,144 @@ +package com.oraclequantapi.oraclequantapi.service; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import com.oraclequantapi.oraclequantapi.model.Sequence; +import com.oraclequantapi.oraclequantapi.repositories.HistoryRecordRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class SequenceService { + + private static final Logger log = LoggerFactory.getLogger(SequenceService.class); + + private final HistoryRecordRepository repo; + + public SequenceService(HistoryRecordRepository repo) { + this.repo = repo; + } + + @Transactional + public HistoryRecord process(String input, String sourceIpAddress) { + log.debug("Processing sequence input='{}' from {}", input, sourceIpAddress); + if (!Sequence.isValid(input)) { + log.warn("Rejecting invalid input '{}'", input); + throw new IllegalArgumentException( + "input must be non-empty and contain only letters and underscores"); + } + List output = parse(input.toLowerCase()); + HistoryRecord record = new HistoryRecord( + null, + LocalDateTime.now(), + sourceIpAddress, + input, + output + ); + HistoryRecord saved = repo.save(record); + log.info("Saved history record id={} input='{}' output={}", + saved.getId(), saved.getInput(), saved.getOutput()); + return saved; + } + + /** Return all history records. */ + public List getAllHistory() { + List all = repo.findAll(); + log.debug("Fetched {} history record(s)", all.size()); + return all; + } + + /** Remove every history record from the database. */ + @Transactional + public void clearHistory() { + log.info("Clearing all history records"); + repo.deleteAll(); + } + + /** Return one history record by id. */ + public Optional getHistoryById(Long id) { + log.debug("Looking up history record id={}", id); + return repo.findById(id); + } + + //Update an existing history record by id. + @Transactional + public Optional updateHistory(Long id, + String newInput, + List newOutput, + String newSourceIpAddress) { + log.debug("Updating history id={} newInput='{}' newOutput={} newIp='{}'", + id, newInput, newOutput, newSourceIpAddress); + + Optional found = repo.findById(id); + if (found.isEmpty()) { + log.warn("History record not found for update: id={}", id); + return Optional.empty(); + } + + List outputToStore = newOutput; + if (newInput != null) { + if (!Sequence.isValid(newInput)) { + log.warn("Rejecting invalid input on update id={}: '{}'", id, newInput); + throw new IllegalArgumentException( + "input must be non-empty and contain only letters and underscores"); + } + if (outputToStore == null) { + outputToStore = parse(newInput.toLowerCase()); + } + } + + HistoryRecord r = found.get(); + if (newInput != null) { + r.setInput(newInput); + } + if (outputToStore != null) { + r.setOutput(outputToStore); + } + if (newSourceIpAddress != null) { + r.setSourceIpAddress(newSourceIpAddress); + } + HistoryRecord saved = repo.save(r); + log.info("History record updated: id={}", id); + return Optional.of(saved); + } + + // ---------------- parsing helpers ---------------- + + private List parse(String s) { + List packages = new ArrayList<>(); + int[] cursor = {0}; + int n = s.length(); + while (cursor[0] < n) { + int packageSize = readZChain(s, cursor); + int sum = 0; + for (int v = 0; v < packageSize && cursor[0] < n; v++) { + sum += readZChain(s, cursor); + } + packages.add(sum); + } + return packages; + } + + private int readZChain(String s, int[] cursor) { + int n = s.length(); + int value = 0; + while (cursor[0] < n && s.charAt(cursor[0]) == 'z') { + value += 26; + cursor[0]++; + } + if (cursor[0] < n) { + char c = s.charAt(cursor[0]); + if (c >= 'a' && c <= 'z') { + value += c - 'a' + 1; + } + cursor[0]++; + } + return value; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 99d0060..0fd9732 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,24 @@ -spring.application.name=oraclequantapi +spring.application.name=sequence-api +server.port=8080 + +# Jackson: keep LocalDateTime as ISO-8601 strings +# (e.g. "2026-05-23T10:15:30") instead of a numeric array. +spring.jackson.serialization.write-dates-as-timestamps=false + +# ---------------- Oracle XE 21c connection ---------------- +# Local Oracle XE 21c reachable at localhost:1521, pluggable DB "XEPDB1". +# Override these in your own application-local.properties (or env vars) if +# your install differs. +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 ---------------- +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect +# "update" lets Hibernate create / extend the tables on first startup. +# Use "validate" or "none" in production. +spring.jpa.hibernate.ddl-auto=update +# Print SQL statements to the log — handy while learning. +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..f91753d --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + ${LOG_DIR}/sequence-api.log + + + ${LOG_DIR}/sequence-api.%d{yyyy-MM-dd}.log + + 7 + + + %d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n + + + + + + + + + + + + + diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..4c32fc0 --- /dev/null +++ b/version.txt @@ -0,0 +1,8 @@ +Sequence API — Changelog + +v1.0 - Initial sequence API +v1.1 - Added history records (id, timestamp, sourceIpAddress, input, output) +v1.2 - Added PUT update endpoint for history records +v1.3 - Added DELETE history endpoint to clear all records +v1.4 - Added logging system (console + rolling daily files, 7-day retention) +v1.5 - Added Oracle XE persistence (Spring Data JPA + Hibernate)