diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..59d0c58 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"9a40df83-1a1a-48b8-9360-9005af8dc622","pid":32748,"acquiredAt":1780899400896} \ No newline at end of file diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..423e36b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +target/ +logs/ +.git/ +.idea/ +*.iml +.mvn/ +mvnw +mvnw.cmd diff --git a/DATABASE_SETUP.md b/DATABASE_SETUP.md new file mode 100644 index 0000000..369ebea --- /dev/null +++ b/DATABASE_SETUP.md @@ -0,0 +1,128 @@ +# Database Setup & Connection Guide + +This project uses an **Oracle Database XE 21c** instance running in Docker. The +application user/schema (`oraclequant`) has already been created inside the +`XEPDB1` pluggable database. + +## 1. Running database container + +A container named `oracle-xe` is already running: + +| Setting | Value | +|----------------|-----------------------------------------------------| +| Image | `container-registry.oracle.com/database/express:latest` | +| Container name | `oracle-xe` | +| Host port | `1521` → container `1521` (DB listener) | +| Host port | `5500` → container `5500` (EM Express, optional) | +| ORACLE_SID | `XE` | +| PDB | `XEPDB1` | +| SYS/SYSTEM pwd | `29999login` | + +If the container isn't running, start it with: + +```bash +docker start oracle-xe +``` + +## 2. Application schema (already created) + +The application connects as the `oraclequant` user inside the `XEPDB1` PDB — +matching `src/main/resources/application.properties`: + +```properties +spring.datasource.url=jdbc:oracle:thin:@localhost:1521/XEPDB1 +spring.datasource.username=oraclequant +spring.datasource.password=oraclequant +``` + +This user was created with: + +```sql +ALTER SESSION SET CONTAINER = XEPDB1; + +CREATE USER oraclequant IDENTIFIED BY oraclequant + DEFAULT TABLESPACE USERS + TEMPORARY TABLESPACE TEMP + QUOTA UNLIMITED ON USERS; + +GRANT CONNECT, RESOURCE TO oraclequant; +GRANT CREATE SESSION, CREATE TABLE, CREATE SEQUENCE, CREATE VIEW, CREATE PROCEDURE TO oraclequant; +``` + +(Spring's `spring.jpa.hibernate.ddl-auto=update` will create/update the actual +tables on first application startup.) + +## 3. Connection details (for any DB GUI tool) + +| Field | Value | +|-------------------|-----------------------------| +| Host | `localhost` | +| Port | `1521` | +| Connection type | **Service Name** (not SID) | +| Service name | `XEPDB1` | +| Username | `oraclequant` | +| Password | `oraclequant` | +| JDBC URL | `jdbc:oracle:thin:@localhost:1521/XEPDB1` | + +> Note: connect using the **service name** `XEPDB1`, not the SID `XE` — `XE` +> is the container database (CDB), and the application schema lives in the +> pluggable database `XEPDB1`. + +## 4. Connecting with DbVisualizer (Oracle DataDirect driver) + +You already have a connection saved in DbVisualizer called **`Oracle-xe`** +(driver: *Oracle (DataDirect)*), but it currently points at the wrong +service name (`ORCL`) and has no username/password set — that's why it +fails. Fix it like this: + +1. Open DbVisualizer → in the **Databases** tab on the left, select the + **`Oracle-xe`** connection. +2. Open its **Properties** tab (or right-click → **Properties**) and check + the driver is **Oracle (DataDirect)**. +3. On the connection's **Database** / connection settings, set: + - **Server**: `localhost` + - **Port**: `1521` + - **Service Name**: `XEPDB1` ← change this from `ORCL` to `XEPDB1` + - **Database Userid**: `oraclequant` + - **Database Password**: `oraclequant` +4. Click **Connect** (the plug icon, or right-click → **Connect**). +5. Once connected, expand `Oracle-xe` → **Schemas** → **ORACLEQUANT** to + browse tables. Tables appear after the Spring Boot app has run at least + once (Hibernate creates them via `ddl-auto=update`). + +### If you'd rather create a fresh connection from scratch + +1. **Connection → Create Connection...** +2. Pick **Oracle** as the database type, then choose driver + **Oracle (DataDirect)** when prompted. +3. Name it (e.g. `oraclequant-xepdb1`), then on the connection settings tab + fill in: + - **Server**: `localhost` + - **Port**: `1521` + - **Service Name**: `XEPDB1` *(NOT `XE` / `ORCL` — that's the CDB SID, the + app schema lives in the pluggable database `XEPDB1`)* + - **Database Userid**: `oraclequant` + - **Database Password**: `oraclequant` +4. Click **Ping Server** to verify connectivity, then **Connect**. + +> The Oracle DataDirect driver builds the JDBC URL from these fields itself +> — you don't need to type a URL manually. If DbVisualizer asks you to +> download/install the driver the first time, allow it. + +## 5. Connecting via SQL*Plus (CLI, inside the container) + +```bash +docker exec -it oracle-xe sqlplus oraclequant/oraclequant@localhost:1521/XEPDB1 +``` + +## 6. Troubleshooting + +- **ORA-12514 "TNS:listener does not currently know of service"**: the + **Service Name** field has the wrong value (e.g. `ORCL` or `XE`). Set it + to `XEPDB1`. +- **ORA-01017 "invalid username/password"**: double check **Database + Userid**/**Database Password** are filled in (`oraclequant`/`oraclequant`) + and that you're targeting `XEPDB1` — the `oraclequant` user only exists + inside `XEPDB1`, not in the root `XE` container. +- **Connection refused**: make sure the container is running and healthy — + `docker ps` should show `oracle-xe` as `Up ... (healthy)`. diff --git a/DOCKER.md b/DOCKER.md new file mode 100644 index 0000000..b5fbb70 --- /dev/null +++ b/DOCKER.md @@ -0,0 +1,106 @@ +# Running Everything in Docker (Database + App) + +`docker-compose.yml` spins up **both** the Oracle XE database and the +Spring Boot app as containers, networked together — no local Java/Maven/JDK +setup required. + +## 1. Files involved + +| File | Purpose | +|-----------------------------------|----------------------------------------------------------------| +| `Dockerfile` | Multi-stage build: compiles the jar with Maven+JDK17, runs it on a slim JRE 17 | +| `docker-compose.yml` | Defines the `oracle-xe` and `app` services + shared network | +| `db/setup/01_create_oraclequant_user.sql` | Auto-creates the `oraclequant` schema in `XEPDB1` on first DB init | +| `.dockerignore` | Keeps `target/`, `.git/`, `.idea/`, `logs/` out of the build context | + +## 2. IMPORTANT: stop any existing standalone Oracle XE container first + +You already have a separate Oracle XE container (`oracle-xe`, started from +`Documents/oracle-xe-db-hosting/docker-compose.yaml`) bound to host port +`1521`. It will conflict with the one in this stack. Stop it first: + +```bash +docker compose -f "C:\Users\kinda\OneDrive - yay app\Documents\oracle-xe-db-hosting\docker-compose.yaml" down +``` +*(This only stops that container — its data volume is untouched.)* + +## 3. Start the full stack + +From the project root: + +```bash +docker compose up --build +``` + +What happens: +1. **`oracle-xe`** starts from a fresh `oracle-data` volume. On its very + first initialization (this can take several minutes — Oracle XE creates + the database from scratch), it automatically executes + `db/setup/01_create_oraclequant_user.sql`, creating the `oraclequant` + user/schema inside `XEPDB1` — exactly what the app needs. +2. Once `oracle-xe` reports **healthy**, the **`app`** service builds (Maven + compiles the jar inside a build stage) and starts, connecting to + `jdbc:oracle:thin:@oracle-xe:1521/XEPDB1` as `oraclequant`/`oraclequant` + (passed via `DB_URL`/`DB_USERNAME`/`DB_PASSWORD` env vars — see + `application.properties`, which reads these with `oraclequant` as default). +3. Hibernate (`ddl-auto=update`) creates `HISTORY_RECORD` automatically on + first startup. + +To run in the background: +```bash +docker compose up --build -d +``` + +## 4. Verify it's running + +```bash +docker compose ps +# both `oraclequant-db` and `oraclequantapi` should show as Up/healthy + +curl -s -G "http://localhost:8080/convert-measurements" --data-urlencode "input=aa" +# -> [1] + +curl -s http://localhost:8080/history +``` + +See `TESTING.md` for a full set of endpoint test cases. + +## 5. Logs + +```bash +docker compose logs -f app # Spring Boot / app logs +docker compose logs -f oracle-xe # Database logs +``` + +The app also writes to `./logs/oraclequantapi.log` on the host (mounted via +`volumes: - ./logs:/app/logs`). + +## 6. Connecting a DB GUI tool (DbVisualizer, etc.) + +Same connection details as before — Docker still publishes port `1521` on +`localhost`: +- Host: `localhost`, Port: `1521`, Service Name: `XEPDB1` +- User: `oraclequant` / Password: `oraclequant` + +See `DATABASE_SETUP.md` for full GUI connection steps. + +## 7. Stopping / cleaning up + +```bash +docker compose down # stop + remove containers (keeps the data volume) +docker compose down -v # also delete the database volume (full reset) +``` + +## 8. Troubleshooting + +- **"port is already allocated" for 1521/8080**: another container or local + process is using the port. Check `docker ps` and stop the conflicting + container (see step 2), or stop a locally-running instance of the app. +- **`app` keeps restarting / can't connect to DB**: the database can take + several minutes to initialize on first run. Watch + `docker compose logs -f oracle-xe` until it reports healthy before the + app's `depends_on: condition: service_healthy` lets it start. +- **`oraclequant` user missing after first run**: the setup script in + `db/setup/` only runs against an *empty* `oracle-data` volume. If you've + run the stack before with old data, either `docker compose down -v` for a + clean slate, or create the user manually (see `DATABASE_SETUP.md` §2). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..548249a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# ---- Build stage: compile the Spring Boot jar with Maven + JDK 17 ---- +FROM maven:3.9.6-eclipse-temurin-17 AS build +WORKDIR /build + +COPY pom.xml . +RUN mvn -q dependency:go-offline + +COPY src ./src +RUN mvn -q clean package -DskipTests \ + && find target -maxdepth 1 -name "*.jar" ! -name "*.original" -exec cp {} app.jar \; + +# ---- Runtime stage: run the jar on a slim JRE 17 ---- +FROM eclipse-temurin:17-jre-jammy +WORKDIR /app + +COPY --from=build /build/app.jar app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..e7dcd6e --- /dev/null +++ b/TESTING.md @@ -0,0 +1,110 @@ +# Testing the `/convert-measurements` Endpoint + +The app exposes a single `GET` endpoint that converts a measurement string +into a list of integer totals, and records every request in the +`HISTORY_RECORD` table (browsable via the `/history` endpoints). + +Base URL (when running locally or via Docker Compose, see `DOCKER.md`): +``` +http://localhost:8080 +``` + +## 1. Endpoint + +``` +GET /convert-measurements?input= +``` + +Returns a JSON array of integers, e.g. `[1]`, `[2,2]`, or `[]`. + +## 2. Quick test via browser + +Just open in your browser (spaces must be URL-encoded as `%20`): + +``` +http://localhost:8080/convert-measurements?input=aa +http://localhost:8080/convert-measurements?input=ab%20ab +``` + +## 3. Quick test via curl + +Use `curl -G --data-urlencode` so spaces/special characters are encoded +correctly: + +```bash +curl -s -G "http://localhost:8080/convert-measurements" --data-urlencode "input=aa" +# -> [1] + +curl -s -G "http://localhost:8080/convert-measurements" --data-urlencode "input=ab ab" +# -> [2,2] +``` + +## 4. Verified test cases (run against the live app) + +Parsing rules recap: letters `a`-`z` carry values 1-26, `_` carries 0. A +package = one COUNT token followed by that many VALUE tokens; the package's +total is the sum of its values. A run of `z`s extends the next token's value +by `26 * (number of z's)`. + +| Input | Output | Why | +|------------|------------|---------------------------------------------------------------------| +| `aa` | `[1]` | count=`a`(1), 1 value `a`(1) → sum 1 | +| `ab` | `[2]` | count=`a`(1), 1 value `b`(2) → sum 2 | +| `abc` | `[2]` | first package sums to 2; `c`(3) starts a package needing 3 values but input ends → incomplete, stop with totals so far | +| `_a` | `[0]` | count=`_`(0) → empty package, total 0; `_` followed immediately by a letter stops processing | +| `_b` | `[0]` | same rule as above | +| `za` | `[]` | `z`+`a` → count = 26·1+1 = 27, needs 27 values but input ends → incomplete, no totals collected yet | +| `zza` | `[]` | `zz`+`a` → count = 26·2+1 = 53 → incomplete | +| `z_` | `[]` | `z`+`_` → count = 26·1+0 = 26 → incomplete | +| `ab ab` | `[2,2]` | two packages separated by a single space, each totalling 2 | +| `aab aaa` | `[]` | a space appears mid-token (after reading count `b`) → invalid input → empty list | +| `aa bb` | `[1]` | first package totals 1; two consecutive spaces stop processing, returning totals collected so far | +| `ba` | `[]` | count=`b`(2) needs 2 values, only 1 available before input ends → incomplete | +| `ca_` | `[]` | count=`c`(3) needs 3 values, only 2 available → incomplete | +| `1a`, `a1` | `[]` | digits aren't valid characters (only `a`-`z`, `_`, space) → invalid input | + +Run them all in one go: + +```bash +for input in "aa" "ab" "abc" "_a" "za" "zza" "z_" "ab ab" "aab aaa" "aa bb" "ba" "ca_"; do + printf "input=[%s] -> " "$input" + curl -s -G "http://localhost:8080/convert-measurements" --data-urlencode "input=$input" + echo +done +``` + +## 5. Inspecting recorded history + +Every call (valid or not) is persisted. Browse it via: + +```bash +# list all recorded requests +curl -s http://localhost:8080/history | jq + +# get one record by id +curl -s http://localhost:8080/history/1 | jq + +# delete all history records +curl -s -X DELETE http://localhost:8080/history -o /dev/null -w "%{http_code}\n" +``` + +Example record shape: +```json +{ + "id": 1, + "timestamp": "2026-06-08T15:49:32.774915", + "sourceIpAddress": "0:0:0:0:0:0:0:1", + "input": "2ab", + "output": "[]" +} +``` + +You can also browse the `HISTORY_RECORD` table directly with DbVisualizer — +see `DATABASE_SETUP.md` for connection steps. + +## 6. Postman / HTTP client + +If you prefer a GUI client, import this as a request: +- Method: `GET` +- URL: `http://localhost:8080/convert-measurements` +- Query param: `input` = `aa` (or any of the test strings above) diff --git a/db/setup/01_create_oraclequant_user.sql b/db/setup/01_create_oraclequant_user.sql new file mode 100644 index 0000000..7ecdb9c --- /dev/null +++ b/db/setup/01_create_oraclequant_user.sql @@ -0,0 +1,9 @@ +ALTER SESSION SET CONTAINER = XEPDB1; + +CREATE USER oraclequant IDENTIFIED BY oraclequant + DEFAULT TABLESPACE USERS + TEMPORARY TABLESPACE TEMP + QUOTA UNLIMITED ON USERS; + +GRANT CONNECT, RESOURCE TO oraclequant; +GRANT CREATE SESSION, CREATE TABLE, CREATE SEQUENCE, CREATE VIEW, CREATE PROCEDURE TO oraclequant; diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3d2a104 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,38 @@ +services: + oracle-xe: + image: container-registry.oracle.com/database/express:latest + container_name: oraclequant-db + ports: + - "1521:1521" + - "5500:5500" + environment: + - ORACLE_PWD=29999login + volumes: + - oracle-data:/opt/oracle/oradata + # Auto-creates the oraclequant user/schema in XEPDB1 the FIRST time the + # database initializes (only runs against an empty oracle-data volume). + - ./db/setup:/opt/oracle/scripts/setup + healthcheck: + test: ["CMD-SHELL", "\"$$ORACLE_BASE/$$CHECK_DB_FILE\" >/dev/null || exit 1"] + interval: 60s + timeout: 30s + start_period: 5m + retries: 5 + + app: + build: . + container_name: oraclequantapi + depends_on: + oracle-xe: + condition: service_healthy + ports: + - "8080:8080" + environment: + - DB_URL=jdbc:oracle:thin:@oracle-xe:1521/XEPDB1 + - DB_USERNAME=oraclequant + - DB_PASSWORD=oraclequant + volumes: + - ./logs:/app/logs + +volumes: + oracle-data: diff --git a/logs/oraclequantapi.log b/logs/oraclequantapi.log new file mode 100644 index 0000000..2eb9969 --- /dev/null +++ b/logs/oraclequantapi.log @@ -0,0 +1,147 @@ +2026-06-09T08:45:32.284+04:00 WARN 35784 --- [oraclequantapi] [HikariPool-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=8h41m20s385ms835µs900ns). +2026-06-09T08:51:49.700+04:00 INFO 4416 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Starting OraclequantapiApplication using Java 17.0.14 with PID 4416 (C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi\target\classes started by kinda in C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi) +2026-06-09T08:51:49.701+04:00 DEBUG 4416 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Running with Spring Boot v3.2.5, Spring v6.1.6 +2026-06-09T08:51:49.703+04:00 INFO 4416 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : No active profile set, falling back to 1 default profile: "default" +2026-06-09T08:51:50.531+04:00 INFO 4416 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-06-09T08:51:50.604+04:00 INFO 4416 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 63 ms. Found 1 JPA repository interface. +2026-06-09T08:51:51.205+04:00 INFO 4416 --- [oraclequantapi] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) +2026-06-09T08:51:51.220+04:00 INFO 4416 --- [oraclequantapi] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-06-09T08:51:51.221+04:00 INFO 4416 --- [oraclequantapi] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20] +2026-06-09T08:51:51.307+04:00 INFO 4416 --- [oraclequantapi] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-06-09T08:51:51.308+04:00 INFO 4416 --- [oraclequantapi] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1553 ms +2026-06-09T08:51:51.575+04:00 INFO 4416 --- [oraclequantapi] [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2026-06-09T08:51:51.655+04:00 INFO 4416 --- [oraclequantapi] [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final +2026-06-09T08:51:51.698+04:00 INFO 4416 --- [oraclequantapi] [main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled +2026-06-09T08:51:52.023+04:00 INFO 4416 --- [oraclequantapi] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer +2026-06-09T08:51:52.057+04:00 INFO 4416 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2026-06-09T08:51:52.451+04:00 INFO 4416 --- [oraclequantapi] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@5280688 +2026-06-09T08:51:52.454+04:00 INFO 4416 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2026-06-09T08:51:52.663+04:00 WARN 4416 --- [oraclequantapi] [main] org.hibernate.orm.deprecation : HHH90000025: OracleDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) +2026-06-09T08:51:53.763+04:00 INFO 4416 --- [oraclequantapi] [main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2026-06-09T08:51:54.476+04:00 INFO 4416 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T08:51:55.085+04:00 INFO 4416 --- [oraclequantapi] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '' +2026-06-09T08:51:55.092+04:00 INFO 4416 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Started OraclequantapiApplication in 5.983 seconds (process running for 11.221) +2026-06-09T09:11:45.804+04:00 WARN 4416 --- [oraclequantapi] [HikariPool-1 housekeeper] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Thread starvation or clock leap detected (housekeeper delta=9m23s364ms237µs700ns). +2026-06-09T11:25:45.551+04:00 INFO 4968 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Starting OraclequantapiApplication v0.0.1-SNAPSHOT using Java 17.0.2 with PID 4968 (C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi\target\oraclequantapi-0.0.1-SNAPSHOT.jar started by kinda in C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi) +2026-06-09T11:25:45.555+04:00 DEBUG 4968 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Running with Spring Boot v3.2.5, Spring v6.1.6 +2026-06-09T11:25:45.556+04:00 INFO 4968 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : No active profile set, falling back to 1 default profile: "default" +2026-06-09T11:25:46.168+04:00 INFO 4968 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-06-09T11:25:46.223+04:00 INFO 4968 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 40 ms. Found 1 JPA repository interface. +2026-06-09T11:25:46.675+04:00 INFO 4968 --- [oraclequantapi] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) +2026-06-09T11:25:46.685+04:00 INFO 4968 --- [oraclequantapi] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-06-09T11:25:46.686+04:00 INFO 4968 --- [oraclequantapi] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20] +2026-06-09T11:25:46.714+04:00 INFO 4968 --- [oraclequantapi] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-06-09T11:25:46.715+04:00 INFO 4968 --- [oraclequantapi] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1082 ms +2026-06-09T11:25:46.992+04:00 INFO 4968 --- [oraclequantapi] [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2026-06-09T11:25:47.031+04:00 INFO 4968 --- [oraclequantapi] [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final +2026-06-09T11:25:47.062+04:00 INFO 4968 --- [oraclequantapi] [main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled +2026-06-09T11:25:47.261+04:00 INFO 4968 --- [oraclequantapi] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer +2026-06-09T11:25:47.300+04:00 INFO 4968 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2026-06-09T11:25:47.650+04:00 INFO 4968 --- [oraclequantapi] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@11e834ad +2026-06-09T11:25:47.653+04:00 INFO 4968 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2026-06-09T11:25:47.785+04:00 WARN 4968 --- [oraclequantapi] [main] org.hibernate.orm.deprecation : HHH90000025: OracleDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) +2026-06-09T11:25:48.395+04:00 INFO 4968 --- [oraclequantapi] [main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2026-06-09T11:25:49.112+04:00 INFO 4968 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T11:25:49.666+04:00 WARN 4968 --- [oraclequantapi] [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop' +2026-06-09T11:25:49.667+04:00 INFO 4968 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T11:25:49.670+04:00 INFO 4968 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... +2026-06-09T11:25:49.692+04:00 INFO 4968 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. +2026-06-09T11:25:49.709+04:00 INFO 4968 --- [oraclequantapi] [main] .s.b.a.l.ConditionEvaluationReportLogger : + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2026-06-09T11:25:49.727+04:00 ERROR 4968 --- [oraclequantapi] [main] o.s.b.d.LoggingFailureAnalysisReporter : + +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +Web server failed to start. Port 8080 was already in use. + +Action: + +Identify and stop the process that's listening on port 8080 or configure this application to listen on another port. + +2026-06-09T11:26:49.081+04:00 INFO 26860 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Starting OraclequantapiApplication v0.0.1-SNAPSHOT using Java 17.0.2 with PID 26860 (C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi\target\oraclequantapi-0.0.1-SNAPSHOT.jar started by kinda in C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi) +2026-06-09T11:26:49.084+04:00 DEBUG 26860 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Running with Spring Boot v3.2.5, Spring v6.1.6 +2026-06-09T11:26:49.085+04:00 INFO 26860 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : No active profile set, falling back to 1 default profile: "default" +2026-06-09T11:26:49.646+04:00 INFO 26860 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-06-09T11:26:49.695+04:00 INFO 26860 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 40 ms. Found 1 JPA repository interface. +2026-06-09T11:26:50.175+04:00 INFO 26860 --- [oraclequantapi] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) +2026-06-09T11:26:50.185+04:00 INFO 26860 --- [oraclequantapi] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-06-09T11:26:50.185+04:00 INFO 26860 --- [oraclequantapi] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20] +2026-06-09T11:26:50.213+04:00 INFO 26860 --- [oraclequantapi] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-06-09T11:26:50.214+04:00 INFO 26860 --- [oraclequantapi] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1065 ms +2026-06-09T11:26:50.480+04:00 INFO 26860 --- [oraclequantapi] [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2026-06-09T11:26:50.530+04:00 INFO 26860 --- [oraclequantapi] [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final +2026-06-09T11:26:50.557+04:00 INFO 26860 --- [oraclequantapi] [main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled +2026-06-09T11:26:50.785+04:00 INFO 26860 --- [oraclequantapi] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer +2026-06-09T11:26:50.829+04:00 INFO 26860 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2026-06-09T11:26:51.164+04:00 INFO 26860 --- [oraclequantapi] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@2fee69a1 +2026-06-09T11:26:51.166+04:00 INFO 26860 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2026-06-09T11:26:51.315+04:00 WARN 26860 --- [oraclequantapi] [main] org.hibernate.orm.deprecation : HHH90000025: OracleDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) +2026-06-09T11:26:51.910+04:00 INFO 26860 --- [oraclequantapi] [main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2026-06-09T11:26:52.235+04:00 INFO 26860 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T11:26:52.761+04:00 WARN 26860 --- [oraclequantapi] [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop' +2026-06-09T11:26:52.763+04:00 INFO 26860 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T11:26:52.765+04:00 INFO 26860 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... +2026-06-09T11:26:52.785+04:00 INFO 26860 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. +2026-06-09T11:26:52.796+04:00 INFO 26860 --- [oraclequantapi] [main] .s.b.a.l.ConditionEvaluationReportLogger : + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2026-06-09T11:26:52.825+04:00 ERROR 26860 --- [oraclequantapi] [main] o.s.b.d.LoggingFailureAnalysisReporter : + +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +Web server failed to start. Port 8080 was already in use. + +Action: + +Identify and stop the process that's listening on port 8080 or configure this application to listen on another port. + +2026-06-09T11:27:30.849+04:00 INFO 19592 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Starting OraclequantapiApplication v0.0.1-SNAPSHOT using Java 17.0.2 with PID 19592 (C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi\target\oraclequantapi-0.0.1-SNAPSHOT.jar started by kinda in C:\Users\kinda\OneDrive - yay app\Desktop\Java\oraclequantapi) +2026-06-09T11:27:30.851+04:00 DEBUG 19592 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : Running with Spring Boot v3.2.5, Spring v6.1.6 +2026-06-09T11:27:30.852+04:00 INFO 19592 --- [oraclequantapi] [main] c.o.o.OraclequantapiApplication : No active profile set, falling back to 1 default profile: "default" +2026-06-09T11:27:31.466+04:00 INFO 19592 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode. +2026-06-09T11:27:31.508+04:00 INFO 19592 --- [oraclequantapi] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 34 ms. Found 1 JPA repository interface. +2026-06-09T11:27:32.048+04:00 INFO 19592 --- [oraclequantapi] [main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http) +2026-06-09T11:27:32.060+04:00 INFO 19592 --- [oraclequantapi] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat] +2026-06-09T11:27:32.060+04:00 INFO 19592 --- [oraclequantapi] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20] +2026-06-09T11:27:32.102+04:00 INFO 19592 --- [oraclequantapi] [main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext +2026-06-09T11:27:32.102+04:00 INFO 19592 --- [oraclequantapi] [main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1194 ms +2026-06-09T11:27:32.368+04:00 INFO 19592 --- [oraclequantapi] [main] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default] +2026-06-09T11:27:32.429+04:00 INFO 19592 --- [oraclequantapi] [main] org.hibernate.Version : HHH000412: Hibernate ORM core version 6.4.4.Final +2026-06-09T11:27:32.456+04:00 INFO 19592 --- [oraclequantapi] [main] o.h.c.internal.RegionFactoryInitiator : HHH000026: Second-level cache disabled +2026-06-09T11:27:32.677+04:00 INFO 19592 --- [oraclequantapi] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer +2026-06-09T11:27:32.713+04:00 INFO 19592 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting... +2026-06-09T11:27:33.020+04:00 INFO 19592 --- [oraclequantapi] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection oracle.jdbc.driver.T4CConnection@62058742 +2026-06-09T11:27:33.022+04:00 INFO 19592 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed. +2026-06-09T11:27:33.192+04:00 WARN 19592 --- [oraclequantapi] [main] org.hibernate.orm.deprecation : HHH90000025: OracleDialect does not need to be specified explicitly using 'hibernate.dialect' (remove the property setting and it will be selected by default) +2026-06-09T11:27:33.778+04:00 INFO 19592 --- [oraclequantapi] [main] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration) +2026-06-09T11:27:33.931+04:00 INFO 19592 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T11:27:34.473+04:00 WARN 19592 --- [oraclequantapi] [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Failed to start bean 'webServerStartStop' +2026-06-09T11:27:34.474+04:00 INFO 19592 --- [oraclequantapi] [main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default' +2026-06-09T11:27:34.478+04:00 INFO 19592 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated... +2026-06-09T11:27:34.501+04:00 INFO 19592 --- [oraclequantapi] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed. +2026-06-09T11:27:34.513+04:00 INFO 19592 --- [oraclequantapi] [main] .s.b.a.l.ConditionEvaluationReportLogger : + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2026-06-09T11:27:34.529+04:00 ERROR 19592 --- [oraclequantapi] [main] o.s.b.d.LoggingFailureAnalysisReporter : + +*************************** +APPLICATION FAILED TO START +*************************** + +Description: + +Web server failed to start. Port 8080 was already in use. + +Action: + +Identify and stop the process that's listening on port 8080 or configure this application to listen on another port. + diff --git a/logs/oraclequantapi.log.2026-06-08.gz b/logs/oraclequantapi.log.2026-06-08.gz new file mode 100644 index 0000000..7a132e3 Binary files /dev/null and b/logs/oraclequantapi.log.2026-06-08.gz differ diff --git a/pom.xml b/pom.xml index 20909d2..4314af4 100644 --- a/pom.xml +++ b/pom.xml @@ -5,27 +5,14 @@ org.springframework.boot spring-boot-starter-parent - 3.5.14 + 3.2.5 com.oraclequantapi oraclequantapi 0.0.1-SNAPSHOT - - - - - - - - - - - - - - - + oraclequantapi + OracleQuant measurement & history API 17 @@ -35,6 +22,23 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-data-jpa + + + + com.oracle.database.jdbc + ojdbc11 + runtime + + + + org.projectlombok + lombok + true + + org.springframework.boot spring-boot-starter-test @@ -47,6 +51,14 @@ org.springframework.boot spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java b/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java index 5e28689..d7da980 100644 --- a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java +++ b/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java @@ -6,6 +6,7 @@ @SpringBootApplication public class OraclequantapiApplication { + public static void main(String[] args) { SpringApplication.run(OraclequantapiApplication.class, args); } 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..7fadb4e --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controller/HistoryController.java @@ -0,0 +1,59 @@ +package com.oraclequantapi.oraclequantapi.controller; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import com.oraclequantapi.oraclequantapi.service.HistoryService; +import lombok.RequiredArgsConstructor; +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.PatchMapping; +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.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +/** + * Read/write access to the persisted {@code /convert-measurements} request history. + */ +@RestController +@RequestMapping("/history") +@RequiredArgsConstructor +public class HistoryController { + + private final HistoryService historyService; + + @GetMapping + public List getAll() { + return historyService.getAll(); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable Long id) { + return historyService.getById(id) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @PutMapping("/{id}") + public ResponseEntity update(@PathVariable Long id, @RequestBody HistoryRecord record) { + return historyService.update(id, record) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @PatchMapping("/{id}") + public ResponseEntity partialUpdate(@PathVariable Long id, @RequestBody HistoryRecord record) { + return historyService.partialUpdate(id, record) + .map(ResponseEntity::ok) + .orElseGet(() -> ResponseEntity.notFound().build()); + } + + @DeleteMapping + public ResponseEntity deleteAll() { + historyService.deleteAll(); + return ResponseEntity.noContent().build(); + } +} 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..b6cf173 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controller/MeasurementController.java @@ -0,0 +1,46 @@ +package com.oraclequantapi.oraclequantapi.controller; + +import com.oraclequantapi.oraclequantapi.service.HistoryService; +import com.oraclequantapi.oraclequantapi.service.MeasurementService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +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; + +/** + * Exposes the measurement-conversion endpoint. Each request is delegated to + * {@link MeasurementService} for computation and recorded via {@link HistoryService}. + */ +@Slf4j +@RestController +@RequiredArgsConstructor +public class MeasurementController { + + private static final String FORWARDED_FOR_HEADER = "X-Forwarded-For"; + + private final MeasurementService measurementService; + private final HistoryService historyService; + + @GetMapping("/convert-measurements") + public List convertMeasurements(@RequestParam("input") String input, HttpServletRequest request) { + List output = measurementService.convert(input); + + String sourceIpAddress = resolveClientIp(request); + historyService.save(sourceIpAddress, input, output.toString()); + + log.info("Converted measurement input='{}' from {} -> {}", input, sourceIpAddress, output); + return output; + } + + private String resolveClientIp(HttpServletRequest request) { + String forwardedFor = request.getHeader(FORWARDED_FOR_HEADER); + if (forwardedFor != null && !forwardedFor.isBlank()) { + return forwardedFor.split(",")[0].trim(); + } + return request.getRemoteAddr(); + } +} 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..9aa2758 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/model/HistoryRecord.java @@ -0,0 +1,45 @@ +package com.oraclequantapi.oraclequantapi.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.time.LocalDateTime; + +/** + * Persistent record of a single {@code /convert-measurements} request, stored in Oracle XE. + */ +@Entity +@Table(name = "HISTORY_RECORD") +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class HistoryRecord { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "ID") + private Long id; + + @Column(name = "REQUEST_TIMESTAMP", nullable = false) + private LocalDateTime timestamp; + + @Column(name = "SOURCE_IP_ADDRESS", length = 64, nullable = false) + private String sourceIpAddress; + + @Column(name = "INPUT_TEXT", length = 4000, nullable = false) + private String input; + + @Column(name = "OUTPUT_TEXT", length = 4000, nullable = false) + private String output; +} 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..d31986d --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/repository/HistoryRepository.java @@ -0,0 +1,12 @@ +package com.oraclequantapi.oraclequantapi.repository; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +/** + * Spring Data JPA repository for {@link HistoryRecord}, backed by Oracle XE. + */ +@Repository +public interface HistoryRepository extends JpaRepository { +} 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..3aa39ce --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/service/HistoryService.java @@ -0,0 +1,86 @@ +package com.oraclequantapi.oraclequantapi.service; + +import com.oraclequantapi.oraclequantapi.model.HistoryRecord; +import com.oraclequantapi.oraclequantapi.repository.HistoryRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * Owns persistence and CRUD operations for {@link HistoryRecord}s. + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class HistoryService { + + private final HistoryRepository historyRepository; + + @Transactional + public HistoryRecord save(String sourceIpAddress, String input, String output) { + HistoryRecord record = HistoryRecord.builder() + .timestamp(LocalDateTime.now()) + .sourceIpAddress(sourceIpAddress) + .input(input) + .output(output) + .build(); + + HistoryRecord saved = historyRepository.save(record); + log.debug("Saved history record id={} input='{}'", saved.getId(), input); + return saved; + } + + @Transactional(readOnly = true) + public List getAll() { + return historyRepository.findAll(); + } + + @Transactional(readOnly = true) + public Optional getById(Long id) { + return historyRepository.findById(id); + } + + @Transactional + public Optional update(Long id, HistoryRecord replacement) { + return historyRepository.findById(id).map(existing -> { + existing.setTimestamp(replacement.getTimestamp()); + existing.setSourceIpAddress(replacement.getSourceIpAddress()); + existing.setInput(replacement.getInput()); + existing.setOutput(replacement.getOutput()); + log.debug("Fully updated history record id={}", id); + return historyRepository.save(existing); + }); + } + + @Transactional + public Optional partialUpdate(Long id, HistoryRecord patch) { + return historyRepository.findById(id).map(existing -> { + if (patch.getTimestamp() != null) { + existing.setTimestamp(patch.getTimestamp()); + } + if (patch.getSourceIpAddress() != null) { + existing.setSourceIpAddress(patch.getSourceIpAddress()); + } + if (patch.getInput() != null) { + existing.setInput(patch.getInput()); + } + if (patch.getOutput() != null) { + existing.setOutput(patch.getOutput()); + } + log.debug("Partially updated history record id={}", id); + return historyRepository.save(existing); + }); + } + + @Transactional + public void deleteAll() { + long count = historyRepository.count(); + historyRepository.deleteAllInBatch(); + log.info("Deleted {} history record(s)", count); + } +} 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..1f0d88a --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/service/MeasurementService.java @@ -0,0 +1,168 @@ +package com.oraclequantapi.oraclequantapi.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Implements the measurement-conversion algorithm used by {@code /convert-measurements}. + * + *

Parsing rules: + *

    + *
  • Letters a-z carry their alphabet position as a value (a=1 ... z=26) and underscore carries 0.
  • + *
  • A run of consecutive 'z' characters followed by one terminating letter/underscore forms a single + * token whose value is {@code 26 * (number of z's) + value(terminator)} (e.g. za=27, zza=53, z_=26).
  • + *
  • Each "package" is read as a COUNT token followed by that many VALUE tokens; the package's total is + * the sum of its values. Packages are read back-to-back until the input (or a separator) ends them.
  • + *
  • A single space separates packages; two or more consecutive spaces stop processing and the totals + * collected so far are returned as-is.
  • + *
  • Encountering a space while a token is only partially read, or any character outside a-z/_/space, + * invalidates the whole input and an empty list is returned.
  • + *
  • A zero-count package (started by '_') immediately followed by a letter stops processing and the + * totals collected so far (including that zero) are returned.
  • + *
+ */ +@Slf4j +@Service +public class MeasurementService { + + private static final char SEPARATOR = ' '; + private static final char ZERO_CHAR = '_'; + private static final char Z = 'z'; + private static final int ALPHABET_SIZE = 26; + + public List convert(String input) { + if (input == null) { + return Collections.emptyList(); + } + + List totals = new ArrayList<>(); + int length = input.length(); + int index = 0; + + while (index < length) { + char current = input.charAt(index); + + if (current == SEPARATOR) { + if (index + 1 < length && input.charAt(index + 1) == SEPARATOR) { + log.debug("Stopping at consecutive separators for input '{}'", input); + return totals; + } + index++; + continue; + } + + Token countToken = readToken(input, index); + if (countToken.status() == TokenStatus.INVALID || countToken.status() == TokenStatus.SPACE) { + log.debug("Invalid input '{}': malformed package count at index {}", input, index); + return Collections.emptyList(); + } + if (countToken.status() == TokenStatus.END_OF_INPUT) { + return totals; + } + + int count = countToken.value(); + index = countToken.nextIndex(); + + int sum = 0; + boolean incomplete = false; + for (int valueNumber = 0; valueNumber < count; valueNumber++) { + Token valueToken = readToken(input, index); + if (valueToken.status() == TokenStatus.INVALID || valueToken.status() == TokenStatus.SPACE) { + log.debug("Invalid input '{}': malformed package value at index {}", input, index); + return Collections.emptyList(); + } + if (valueToken.status() == TokenStatus.END_OF_INPUT) { + incomplete = true; + break; + } + sum += valueToken.value(); + index = valueToken.nextIndex(); + } + + if (incomplete) { + log.debug("Stopping at incomplete package for input '{}'", input); + return totals; + } + + totals.add(sum); + + if (count == 0 && index < length && isLetter(input.charAt(index))) { + return totals; + } + } + + return totals; + } + + /** + * Reads a single token starting at {@code index}: zero or more 'z' characters followed by exactly + * one terminating character (a letter a-y/z or underscore). The token's value combines the leading + * z-run with the terminator: {@code 26 * zCount + value(terminator)}. + */ + private Token readToken(String input, int index) { + int length = input.length(); + int zCount = 0; + int cursor = index; + + while (cursor < length && input.charAt(cursor) == Z) { + zCount++; + cursor++; + } + + if (cursor >= length) { + return Token.endOfInput(); + } + + char terminator = input.charAt(cursor); + if (terminator == SEPARATOR) { + return Token.space(); + } + + Integer terminatorValue = letterValue(terminator); + if (terminatorValue == null) { + return Token.invalid(); + } + + return Token.ok(ALPHABET_SIZE * zCount + terminatorValue, cursor + 1); + } + + private Integer letterValue(char c) { + if (c == ZERO_CHAR) { + return 0; + } + if (isLetter(c)) { + return c - 'a' + 1; + } + return null; + } + + private boolean isLetter(char c) { + return c >= 'a' && c <= 'z'; + } + + private enum TokenStatus { + OK, SPACE, INVALID, END_OF_INPUT + } + + private record Token(TokenStatus status, int value, int nextIndex) { + static Token ok(int value, int nextIndex) { + return new Token(TokenStatus.OK, value, nextIndex); + } + + static Token space() { + return new Token(TokenStatus.SPACE, 0, -1); + } + + static Token invalid() { + return new Token(TokenStatus.INVALID, 0, -1); + } + + static Token endOfInput() { + return new Token(TokenStatus.END_OF_INPUT, 0, -1); + } + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 99d0060..255ea14 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,27 @@ spring.application.name=oraclequantapi + +# =============================== +# Oracle XE Datasource (Docker, port 1521) +# =============================== +spring.datasource.url=jdbc:oracle:thin:@192.168.100.148:1521/XEPDB1 +spring.datasource.username=system +spring.datasource.password=29999login +spring.datasource.driver-class-name=oracle.jdbc.OracleDriver + +# =============================== +# JPA / Hibernate +# =============================== +spring.jpa.hibernate.ddl-auto=update +spring.jpa.open-in-view=false +spring.jpa.show-sql=true +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect + +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.OracleDialect +spring.jpa.properties.hibernate.format_sql=true + +# =============================== +# Logging (see logback-spring.xml for console + rolling file appenders) +# =============================== +logging.file.name=logs/oraclequantapi.log +logging.level.root=INFO +logging.level.com.oraclequantapi.oraclequantapi=DEBUG diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..5514a19 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,34 @@ + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + utf8 + + + + + + ${LOG_FILE} + + ${FILE_LOG_PATTERN} + utf8 + + + ${LOG_FILE}.%d{yyyy-MM-dd}.gz + 7 + 1GB + + + + + + + + + diff --git a/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java b/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java index 2de285b..9716054 100644 --- a/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java +++ b/src/test/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplicationTests.java @@ -6,6 +6,7 @@ @SpringBootTest class OraclequantapiApplicationTests { + @Test void contextLoads() { } diff --git a/src/test/java/com/oraclequantapi/oraclequantapi/service/MeasurementServiceTest.java b/src/test/java/com/oraclequantapi/oraclequantapi/service/MeasurementServiceTest.java new file mode 100644 index 0000000..e964a8e --- /dev/null +++ b/src/test/java/com/oraclequantapi/oraclequantapi/service/MeasurementServiceTest.java @@ -0,0 +1,50 @@ +package com.oraclequantapi.oraclequantapi.service; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class MeasurementServiceTest { + + private final MeasurementService measurementService = new MeasurementService(); + + static Stream cases() { + return Stream.of( + Arguments.of("aa", List.of(1)), + Arguments.of("abbcc", List.of(2, 6)), + Arguments.of("dz_a_aazzaaa", List.of(28, 53, 1)), + Arguments.of("a_", List.of(0)), + Arguments.of("abcdabcdab", List.of(2, 7, 7)), + Arguments.of("abcdabcdab_", List.of(2, 7, 7, 0)), + Arguments.of("zdaaaaaaaabaaaaaaaabaaaaaaaabbaa", List.of(34)), + Arguments.of("zza_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_a_", List.of(26)), + Arguments.of("za_a_a_a_a_a_a_a_a_a_a_a_a_azaaa", List.of(40, 1)), + Arguments.of("_", List.of(0)), + Arguments.of("_ad", List.of(0)), + Arguments.of("_zzzb", List.of(0)), + Arguments.of("__", List.of(0, 0)), + Arguments.of("_ _", List.of(0, 0)), + Arguments.of(" ", List.of()), + Arguments.of(" _ _ ", List.of(0, 0)), + Arguments.of(" _", List.of()), + Arguments.of("_ _", List.of(0)), + Arguments.of("z ab_", List.of()), + Arguments.of("ba za", List.of()), + Arguments.of("za@bcd", List.of()), + Arguments.of("za!", List.of()), + Arguments.of("ab_ ab_", List.of(2, 0)), + Arguments.of("ab_ ab_", List.of(2, 0)) + ); + } + + @ParameterizedTest(name = "convert(\"{0}\") = {1}") + @MethodSource("cases") + void convertProducesExpectedTotals(String input, List expected) { + assertThat(measurementService.convert(input)).isEqualTo(expected); + } +} diff --git a/target/oraclequantapi-0.0.1-SNAPSHOT.jar b/target/oraclequantapi-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000..c02ed78 Binary files /dev/null and b/target/oraclequantapi-0.0.1-SNAPSHOT.jar differ