diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..8dd423d --- /dev/null +++ b/Changelog.md @@ -0,0 +1,53 @@ +# Changelog + +All notable changes to the Measurement Conversion API will be documented in this file. + +--- +## [1.0.0] - 2026-05-21 +## [2.0.0] - 2026-05-25 + +### Added + +* REST API for measurement sequence conversion (`/convert-measurements`) +* Oracle XE integration using Spring Data JPA +* Persistent history tracking in `CONVERSION_HISTORY` table +* Full History CRUD endpoints under `/input` +* Sequence parsing engine with counter-based tokenization +* Support for `'z'` chaining logic in decoding algorithm + +### Changed + +* Improved `SequenceService` logic for package decoding +* Refactored history update endpoints to correctly use `SequenceHistory` +* Enhanced logging for request tracing and debugging + +### Fixed + +* Fixed incorrect request body type in HistoryController (`SequenceHistory` instead of `Sequence`) +* Fixed update/patch logic consistency in `HistoryService` + +### Security Notes + +* Database credentials currently stored in `application.properties` (should be externalized in production) + +--- + +## [1.0.0] - Initial Prototype + +### Added + +* Basic sequence parsing model +* In-memory sequence repository +* Initial REST controller structure + +--- + +## Planned + +* Input validation improvements +* Global exception handling (`@ControllerAdvice`) +* OpenAPI/Swagger documentation +* Docker support +* Authentication layer (JWT or Basic Auth) + +--- diff --git a/README.md b/README.md index b1cccfd..c11528a 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,163 @@ -## 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. +# Measurement Conversion API + +A Spring Boot REST API that parses and converts encoded measurement sequences into numeric results and stores request history in an Oracle XE database. + +--- + +## Overview + +This service exposes endpoints to: + +* Parse encoded measurement input strings +* Convert them into numeric package totals +* Store every request in an Oracle database +* Retrieve, update, and delete conversion history + +--- + +## Setup + +* Java 17 +* Spring Boot 3.5.x +* Spring Web +* Spring Data JPA +* Oracle XE (JDBC) +* Maven + +--- + +## API Endpoints + +### Convert Measurements + +**GET** `/convert-measurements?input={string}` + +Converts encoded measurement input into numeric results. + +**Example:** + +``` +GET /convert-measurements?input=abczz_a +``` + +**Response:** + +```json +[3, 28] +``` + +--- + +### History API + +Base path: `/input` + +#### Get all history + +``` +GET /input +``` + +#### Get history by ID + +``` +GET /input/{id} +``` + +#### Full update (PUT) + +``` +PUT /input/{id} +``` + +#### Partial update (PATCH) + +``` +PATCH /input/{id} +``` + +#### Delete all history + +``` +DELETE /input +``` + +--- + +## Encoding Rules + +* `a = 1, b = 2, ..., z = 26` +* `_ = 0` +* First character of each package = counter (number of value slots) +* `'z'` introduces chaining: adds 26 + next character value recursively + +Example: + +``` +dz_a_a → 28 +``` + +--- + +## Database Setup + +Uses Oracle XE with JPA. + +Table: `MEASUREMENT_HISTORY` + +Fields: + +* ID (UUID) +* TIMESTAMP +* SOURCE_IP_ADDRESS +* INPUT +* OUTPUT + +--- + +## Configuration + +Update `application.properties`: + +```properties +spring.datasource.url=jdbc:oracle:thin:@:1521/XEPDB1 +spring.datasource.username=system +spring.datasource.password=your_password +``` + +--- + +## Run Application + +```bash +mvn spring-boot:run +``` + +Or: + +```bash +mvn clean package +java -jar target/measurement-conversion-2.0.0.jar +``` + +--- + +## Project Structure + +* `controllers` → REST endpoints +* `services` → business logic +* `models` → domain + entity classes +* `repositories` → JPA + in-memory store + +--- + +## Logging + +* Request logging via SLF4J +* Debug logs for parsing and conversion steps + +--- + +## License + +Internal / educational use (update as needed). diff --git a/pom.xml b/pom.xml index 20909d2..13e93b0 100644 --- a/pom.xml +++ b/pom.xml @@ -1,16 +1,17 @@ + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 org.springframework.boot spring-boot-starter-parent 3.5.14 - + + - com.oraclequantapi - oraclequantapi - 0.0.1-SNAPSHOT + om.measurement + measurement-conversion + 2.0.0 @@ -35,6 +36,19 @@ spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + com.oracle.database.jdbc + ojdbc11 + runtime + + org.springframework.boot spring-boot-starter-test diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java b/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java index 5e28689..87dd125 100644 --- a/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java +++ b/src/main/java/com/oraclequantapi/oraclequantapi/OraclequantapiApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.ComponentScan; @SpringBootApplication +@ComponentScan(basePackages = "com.oraclequantapi.oraclequantapi") public class OraclequantapiApplication { public static void main(String[] args) { diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controllers/ConversionController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/ConversionController.java new file mode 100644 index 0000000..345a4a6 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/ConversionController.java @@ -0,0 +1,55 @@ +package com.oraclequantapi.oraclequantapi.controllers; + +import com.oraclequantapi.oraclequantapi.models.Sequence; +import com.oraclequantapi.oraclequantapi.models.SequenceHistory; +import com.oraclequantapi.oraclequantapi.services.HistoryService; +import com.oraclequantapi.oraclequantapi.services.SequenceService; +import jakarta.servlet.http.HttpServletRequest; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDateTime; +import java.util.List; + + +@RestController +@RequestMapping("/convert-measurements") +public class ConversionController { + + private static final Logger logger = LoggerFactory.getLogger(ConversionController.class); + + @Autowired + private SequenceService sequenceService; + + @Autowired + private HistoryService historyService; + + //endpoint "input" + @GetMapping + public ResponseEntity> convertMeasurements( + @RequestParam("input") String input, + HttpServletRequest request) { + + logger.info("GET /measurements?input='{}' | ip='{}'", + input, request.getRemoteAddr()); + + Sequence sequence = sequenceService.getSequence(input); + List result = sequenceService.processSequence(sequence); + + historyService.save(new SequenceHistory( + null, + LocalDateTime.now(), + request.getRemoteAddr(), + input, + result.toString() + )); + + return ResponseEntity.ok(result); + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/controllers/HistoryController.java b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/HistoryController.java new file mode 100644 index 0000000..8f725a1 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/controllers/HistoryController.java @@ -0,0 +1,74 @@ +package com.oraclequantapi.oraclequantapi.controllers; + + +import com.oraclequantapi.oraclequantapi.models.SequenceHistory; +import com.oraclequantapi.oraclequantapi.services.HistoryService; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import org.slf4j.Logger; + +@RestController +@RequestMapping("/input") +public class HistoryController { + + private static final Logger logger = LoggerFactory.getLogger(HistoryController.class); + + @Autowired + private HistoryService historyService; + + @GetMapping + public ResponseEntity> getAll() { + logger.info("GET /input"); + return ResponseEntity.ok(historyService.getAll()); + } + + @GetMapping("/{id}") + public ResponseEntity getById(@PathVariable String id) { + logger.info("GET /input/{}", id); + return historyService.getById(id) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PutMapping("/{id}") + public ResponseEntity update ( + @PathVariable String id, + @RequestBody SequenceHistory updated) { + logger.info("PUT /input/{}", id); + return historyService.update(id, updated) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @PatchMapping("/{id}") + public ResponseEntity patch ( + @PathVariable String id, + @RequestBody SequenceHistory patch){ + logger.info("PATCH /input/{}", id); + return historyService.patch(id, patch) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + //delete all records + @DeleteMapping + public ResponseEntity deleteAll () { + logger.warn("DELETE /input — clearing all records"); + historyService.deleteAll(); + return ResponseEntity.noContent().build(); + } + + //delete one records + @DeleteMapping("/{id}") + public ResponseEntity deleteById (@PathVariable String id){ + logger.warn("DELETE /input/{}", id); + if (historyService.getById(id).isEmpty()) { + return ResponseEntity.notFound().build(); + } + historyService.deleteById(id); + return ResponseEntity.noContent().build(); + } + } diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/models/Sequence.java b/src/main/java/com/oraclequantapi/oraclequantapi/models/Sequence.java new file mode 100644 index 0000000..00a4f53 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/models/Sequence.java @@ -0,0 +1,41 @@ +package com.oraclequantapi.oraclequantapi.models; + +import java.util.ArrayList; +import java.util.List; + +public class Sequence { + + private List value; + + public Sequence() { + this.value = new ArrayList<>(); + } + + public Sequence(List value) { + this.value = new ArrayList<>(value); + } + + //UML methods + public void setValue(List value) { + this.value = new ArrayList<>(value); + } + + public String getValueAsStr() { + return String.join(",", value); + } + + public boolean isValid() { + if (value == null || value.isEmpty()) return false; + for (String token : value) { + for (char c : token.toCharArray()) { + if (c != '_' && (c < 'a' || c > 'z')) + return false; + } + } + return true; + } + + public List getValue() { + return value; + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/models/SequenceHistory.java b/src/main/java/com/oraclequantapi/oraclequantapi/models/SequenceHistory.java new file mode 100644 index 0000000..fd5b570 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/models/SequenceHistory.java @@ -0,0 +1,77 @@ +package com.oraclequantapi.oraclequantapi.models; + + +import java.time.LocalDateTime; +import jakarta.persistence.*; + +@Entity +@Table(name = "MEASUREMENT_HISTORY") + +public class SequenceHistory { + + @Id + @Column(name = "ID", length = 36) + private String 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 = 4000) + private String input; + + @Column(name = "OUTPUT", length = 4000) + private String output; + + public SequenceHistory(){ + } + + public SequenceHistory(String id, + LocalDateTime timestamp, + String sourceIpAddress, + String input, + String output){ + this.id = id; + this.timestamp = timestamp; + this.sourceIpAddress = sourceIpAddress; + this.input = input; + this.output = output; + } + + public String getId(){ + return id; + } + public void setId(String 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 String getOutput(){ + return output; + } + public void setOutput(String output){ + this.output = output; + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/repositories/ConversionHistoryRepository.java b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/ConversionHistoryRepository.java new file mode 100644 index 0000000..81c0565 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/ConversionHistoryRepository.java @@ -0,0 +1,9 @@ +package com.oraclequantapi.oraclequantapi.repositories; + +import com.oraclequantapi.oraclequantapi.models.SequenceHistory; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface ConversionHistoryRepository extends JpaRepository { +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/repositories/SequenceRepository.java b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/SequenceRepository.java new file mode 100644 index 0000000..c526359 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/repositories/SequenceRepository.java @@ -0,0 +1,30 @@ +package com.oraclequantapi.oraclequantapi.repositories; + +import com.oraclequantapi.oraclequantapi.models.Sequence; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.List; + +@Repository +public class SequenceRepository { + + private final List sequenceList = new ArrayList<>(); + + public boolean saveSequence(Sequence sequence){ + if (sequence == null || !sequence.isValid()) + return false; + sequenceList.add(sequence); + return true; + } + + //return all stored sequence obj + public List getAll(){ + return new ArrayList<>(sequenceList); + } + + //clear in-memory list + public void clear(){ + sequenceList.clear(); + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/services/HistoryService.java b/src/main/java/com/oraclequantapi/oraclequantapi/services/HistoryService.java new file mode 100644 index 0000000..74f9585 --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/services/HistoryService.java @@ -0,0 +1,71 @@ +package com.oraclequantapi.oraclequantapi.services; + +import com.oraclequantapi.oraclequantapi.models.SequenceHistory; +import com.oraclequantapi.oraclequantapi.repositories.ConversionHistoryRepository; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import org.slf4j.Logger; + +@Service +public class HistoryService { + + private static final Logger logger = LoggerFactory.getLogger(HistoryService.class); + + @Autowired + private ConversionHistoryRepository historyRepository; + + public SequenceHistory save(SequenceHistory record){ + record.setId(UUID.randomUUID().toString()); + SequenceHistory saved = historyRepository.save(record); + logger.info("History record saved: {}", saved.getId()); + return saved; + } + + public List getAll(){ + logger.info("Fetching all history record"); + return historyRepository.findAll(); + } + + public Optional getById(String id){ + logger.info("Fetching history record: {}", id); + return historyRepository.findById(id); + } + + + public Optional update(String id, SequenceHistory updated) { // ← FIXED + return historyRepository.findById(id).map(existing -> { + existing.setSourceIpAddress(updated.getSourceIpAddress()); + existing.setInput(updated.getInput()); + existing.setOutput(updated.getOutput()); + existing.setTimestamp(updated.getTimestamp()); + logger.info("History record fully updated: {}", id); + return historyRepository.save(existing); + }); + } + + public Optional patch(String id, SequenceHistory patch) { // ← FIXED + return historyRepository.findById(id).map(existing -> { + if (patch.getSourceIpAddress() != null) existing.setSourceIpAddress(patch.getSourceIpAddress()); + if (patch.getInput() != null) existing.setInput(patch.getInput()); + if (patch.getOutput() != null) existing.setOutput(patch.getOutput()); + if (patch.getTimestamp() != null) existing.setTimestamp(patch.getTimestamp()); + logger.info("History record patched: {}", id); + return historyRepository.save(existing); + }); + } + + public void deleteAll() { + logger.warn("Clearing ALL history records"); + historyRepository.deleteAll(); + } + + public void deleteById(String id) { + logger.warn("Deleting history record: {}", id); + historyRepository.deleteById(id); + } +} diff --git a/src/main/java/com/oraclequantapi/oraclequantapi/services/SequenceService.java b/src/main/java/com/oraclequantapi/oraclequantapi/services/SequenceService.java new file mode 100644 index 0000000..10a33ad --- /dev/null +++ b/src/main/java/com/oraclequantapi/oraclequantapi/services/SequenceService.java @@ -0,0 +1,122 @@ +package com.oraclequantapi.oraclequantapi.services; + +import com.oraclequantapi.oraclequantapi.models.Sequence; +import com.oraclequantapi.oraclequantapi.repositories.SequenceRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; + + + @Service + public class SequenceService { + + private static final Logger logger = LoggerFactory.getLogger(SequenceService.class); + + @Autowired + private SequenceRepository sequenceRepository; + + public Sequence getSequence(String input) { + logger.debug("Parsing input into Sequence: {}", input); + + List tokens = new ArrayList<>(); + int[] index = {0}; + + while (index[0] < input.length()) { + int start = index[0]; + + // Read the counter character + char counterChar = input.charAt(index[0]); + int counter = charToValue(counterChar); + index[0]++; + + // Advance past exactly `counter` value slots (respecting z-chains) + for (int v = 0; v < counter; v++) { + if (index[0] >= input.length()) break; + // consumeOneSlot moves index past the full z-chain for one slot + consumeOneSlot(input, index); + } + + tokens.add(input.substring(start, index[0])); + } + + Sequence sequence = new Sequence(tokens); + sequenceRepository.saveSequence(sequence); + logger.debug("Sequence created with {} package(s)", tokens.size()); + return sequence; + } + + + public List processSequence(Sequence sequence) { + logger.info("Processing sequence: {}", sequence.getValueAsStr()); + + List results = new ArrayList<>(); + + for (String token : sequence.getValue()) { + int total = decodePackage(token); + results.add(total); + logger.debug("Token '{}' → {}", token, total); + } + + logger.info("Processing result: {}", results); + return results; + } + + + private int decodePackage(String token) { + if (token == null || token.isEmpty()) return 0; + + int[] index = {0}; + + // First character = counter + int counter = charToValue(token.charAt(index[0])); + index[0]++; + + int total = 0; + + for (int v = 0; v < counter; v++) { + if (index[0] >= token.length()) break; + total += readOneSlot(token, index); + } + + return total; + } + + private int readOneSlot(String input, int[] index) { + if (index[0] >= input.length()) return 0; + + char c = input.charAt(index[0]); + index[0]++; + + if (c == 'z') { + // z chains into the very next character as one slot + return 26 + readOneSlot(input, index); + } + + return charToValue(c); + } + + + private void consumeOneSlot(String input, int[] index) { + if (index[0] >= input.length()) return; + + char c = input.charAt(index[0]); + index[0]++; + + if (c == 'z') { + // z chains: consume the next slot too + consumeOneSlot(input, index); + } + // non-z: one character consumed, done + } + + private int charToValue(char c) { + if (c == '_') return 0; + if (c >= 'a' && c <= 'z') return c - 'a' + 1; + throw new IllegalArgumentException("Invalid character in input: '" + c + "'"); + } + } + diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 99d0060..64973f5 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,14 @@ spring.application.name=oraclequantapi + +# Oracle XE DataSource +spring.datasource.url=jdbc:oracle:thin:@192.168.0.105: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.database-platform=org.hibernate.dialect.OracleDialect +spring.jpa.show-sql=true + +logging.level.org.springframework.web.servlet.mvc.method.annotation=DEBUG \ No newline at end of file