From 32dfb6d6691e6f7d252f5f3edcb14c78767352aa Mon Sep 17 00:00:00 2001 From: Milena Serbinova <45200178+milesha@users.noreply.github.com> Date: Mon, 4 Nov 2024 22:05:03 +0100 Subject: [PATCH] Generate Java API Client for Intelligence Service's OpenAPI Specifications (#106) Co-authored-by: Felix T.J. Dietrich Co-authored-by: Felix T.J. Dietrich Co-authored-by: github-actions[bot] --- ...=> generate-application-server-client.yml} | 8 +- .../generate-intelligence-service-client.yml | 100 +++ .gitignore | 5 +- package-lock.json | 153 +++- package.json | 18 +- server/application-server/pom.xml | 80 +- .../de/tum/in/www1/hephaestus/WebConfig.java | 16 + .../intelligenceservice/ApiClient.java | 803 ++++++++++++++++++ .../intelligenceservice/BaseApi.java | 74 ++ .../BeanValidationException.java | 27 + .../JavaTimeFormatter.java | 64 ++ .../RFC3339DateFormat.java | 58 ++ .../ServerConfiguration.java | 59 ++ .../intelligenceservice/ServerVariable.java | 24 + .../intelligenceservice/api/DefaultApi.java | 119 +++ .../intelligenceservice/auth/ApiKeyAuth.java | 62 ++ .../auth/Authentication.java | 14 + .../auth/HttpBasicAuth.java | 38 + .../auth/HttpBearerAuth.java | 56 ++ .../model/ChatRequest.java | 104 +++ .../model/ChatResponse.java | 104 +++ .../model/HTTPValidationError.java | 116 +++ .../model/ValidationError.java | 178 ++++ .../model/ValidationErrorLocInner.java | 70 ++ server/intelligence-service/app/config.py | 13 +- .../app/generate_openapi_yaml.py | 30 + server/intelligence-service/app/main.py | 16 +- server/intelligence-service/app/model.py | 11 +- server/intelligence-service/openapi.yaml | 82 ++ server/intelligence-service/openapitools.json | 7 + 30 files changed, 2484 insertions(+), 25 deletions(-) rename .github/workflows/{generate-api-client.yml => generate-application-server-client.yml} (89%) create mode 100644 .github/workflows/generate-intelligence-service-client.yml create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ApiClient.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BaseApi.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BeanValidationException.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/JavaTimeFormatter.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/RFC3339DateFormat.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerConfiguration.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerVariable.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/api/DefaultApi.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/ApiKeyAuth.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/Authentication.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBasicAuth.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBearerAuth.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatRequest.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatResponse.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/HTTPValidationError.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationError.java create mode 100644 server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationErrorLocInner.java create mode 100644 server/intelligence-service/app/generate_openapi_yaml.py create mode 100644 server/intelligence-service/openapi.yaml create mode 100644 server/intelligence-service/openapitools.json diff --git a/.github/workflows/generate-api-client.yml b/.github/workflows/generate-application-server-client.yml similarity index 89% rename from .github/workflows/generate-api-client.yml rename to .github/workflows/generate-application-server-client.yml index a285796e..6c671cec 100644 --- a/.github/workflows/generate-api-client.yml +++ b/.github/workflows/generate-application-server-client.yml @@ -5,7 +5,7 @@ on: types: [opened, synchronize, labeled, reopened] paths: - 'server/application-server/**' - - '.github/workflows/generate-api-client.yml' + - '.github/workflows/generate-application-server-client.yml' push: paths: - 'server/application-server/openapi.yaml' @@ -15,7 +15,7 @@ on: jobs: generate-api-client: - name: Verify API Specs and Client (add autocommit-openapi label to PR to auto-commit changes) + name: Verify API Specs and Client of the Application Server (add autocommit-openapi label to PR to auto-commit changes) runs-on: ubuntu-latest steps: @@ -40,8 +40,8 @@ jobs: - name: Install dependencies run: npm install - - name: Generate API client - run: npm run generate:api + - name: Generate API client for the application server + run: npm run generate:api:application-server - name: Check for changes in the API id: check_changes diff --git a/.github/workflows/generate-intelligence-service-client.yml b/.github/workflows/generate-intelligence-service-client.yml new file mode 100644 index 00000000..54842180 --- /dev/null +++ b/.github/workflows/generate-intelligence-service-client.yml @@ -0,0 +1,100 @@ +name: OpenAPI + +on: + pull_request: + types: [opened, synchronize, labeled, reopened] + paths: + - 'server/application-server/**' + - '.github/workflows/generate-intelligence-service-client.yml' + push: + paths: + - 'server/intelligence-service/openapi.yaml' + - 'server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/**' + branches: [develop] + workflow_dispatch: + +jobs: + generate-api-client: + name: Verify API Specs and Client of the Intelligence Service (add autocommit-openapi label to PR to auto-commit changes) + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ github.event.pull_request.head.ref }} + persist-credentials: false + fetch-depth: 0 + + - name: Set up Python 3.12 + uses: actions/setup-python@v2 + with: + python-version: 3.12 + + - name: Install dependencies + run: npm install + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + virtualenvs-create: true + virtualenvs-in-project: true + virtualenvs-path: .venv + installer-parallel: true + + - name: Load cached venv + id: cached-poetry-dependencies + uses: actions/cache@v4 + with: + path: .venv + key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }} + + - name: Install Python dependencies + working-directory: server/intelligence-service + run: poetry install --no-interaction --no-root + + - name: Generate API client for the application server + working-directory: server/intelligence-service + run: poetry run npm run generate:api:intelligence-service + + - name: Check for changes in the API + id: check_changes + run: | + echo "Checking for changes in the API client directory..." + git add . + if git diff --cached --quiet; then + echo "No changes detected in the API client directory." + echo "NO_CHANGES_DETECTED=true" >> $GITHUB_ENV + else + echo "Changes detected in the API client directory." + echo "NO_CHANGES_DETECTED=false" >> $GITHUB_ENV + exit 1 + fi + + - name: Commit files + if: ${{ always() && contains(github.event.pull_request.labels.*.name, 'autocommit-openapi') }} + run: | + echo "Committing and pushing changes..." + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + git commit -a -m "chore: update API specs and client" + + - name: Push changes + if: ${{ always() && contains(github.event.pull_request.labels.*.name, 'autocommit-openapi') }} + uses: ad-m/github-push-action@master + with: + github_token: ${{ secrets.GH_PAT }} + branch: ${{ github.event.pull_request.head.ref }} + + - name: Remove autocommit-openapi label + if: ${{ always() && contains(github.event.pull_request.labels.*.name, 'autocommit-openapi') }} + run: | + echo "Removing the autocommit-openapi label..." + curl --silent --fail-with-body -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ + https://api.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/labels/autocommit-openapi diff --git a/.gitignore b/.gitignore index 999c709a..20602f4f 100644 --- a/.gitignore +++ b/.gitignore @@ -168,4 +168,7 @@ node_modules/ application-local.yml -server/application-server/postgres-data/ \ No newline at end of file +server/application-server/postgres-data/ + +# Java-Client creation temporary +tmp \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bdb067ac..23ea9a38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ ], "devDependencies": { "@openapitools/openapi-generator-cli": "2.13.5", - "rimraf": "6.0.1" + "rimraf": "6.0.1", + "shx": "0.3.4" } }, "node_modules/@babel/runtime": { @@ -830,9 +831,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "dev": true, "funding": [ { @@ -917,6 +918,16 @@ "dev": true, "license": "ISC" }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -966,6 +977,19 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/https-proxy-agent": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.4.tgz", @@ -1060,6 +1084,32 @@ "node": ">=12.0.0" } }, + "node_modules/interpret": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -1222,6 +1272,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", @@ -1354,6 +1414,13 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, "node_modules/path-scurry": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", @@ -1400,6 +1467,18 @@ "node": ">= 6" } }, + "node_modules/rechoir": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", + "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", + "dev": true, + "dependencies": { + "resolve": "^1.1.6" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/reflect-metadata": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", @@ -1424,6 +1503,24 @@ "node": ">=0.10.0" } }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/restore-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", @@ -1579,6 +1676,41 @@ "node": ">=8" } }, + "node_modules/shelljs": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", + "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "glob": "^7.0.0", + "interpret": "^1.0.0", + "rechoir": "^0.6.2" + }, + "bin": { + "shjs": "bin/shjs" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/shx": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/shx/-/shx-0.3.4.tgz", + "integrity": "sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.3", + "shelljs": "^0.8.5" + }, + "bin": { + "shx": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -1673,6 +1805,19 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", diff --git a/package.json b/package.json index a543c2ae..086fe497 100644 --- a/package.json +++ b/package.json @@ -4,13 +4,25 @@ "webapp/*" ], "scripts": { - "generate:api:clean": "rimraf webapp/src/app/core/modules/openapi", + "generate:api:application-server:clean": "rimraf webapp/src/app/core/modules/openapi", + "generate:api:intelligence-service:clean": "rimraf server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice", + "generate:api:clean": "npm run generate:api:intelligence-service:clean && npm run generate:api:application-server:clean", + "generate:api:application-server-specs": "cd server/application-server && mvn verify -DskipTests=true -Dapp.profiles=specs", + "generate:api:intelligence-service-specs": "python -m server.intelligence-service.app.generate_openapi_yaml", + "generate:api:specs": "npm run generate:api:application-server-specs && npm run generate:api:intelligence-service-specs", + "generate:api:application-server-client": "npx openapi-generator-cli generate -i server/application-server/openapi.yaml -g typescript-angular -o webapp/src/app/core/modules/openapi --additional-properties fileNaming=kebab-case,withInterfaces=true --generate-alias-as-model", - "generate:api": "npm run generate:api:application-server-specs && npm run generate:api:clean && npm run generate:api:application-server-client" + "generate:api:intelligence-service-client": "npx openapi-generator-cli generate -i server/intelligence-service/openapi.yaml -g java --library resttemplate --api-package de.tum.in.www1.hephaestus.intelligenceservice.api --model-package de.tum.in.www1.hephaestus.intelligenceservice.model --invoker-package de.tum.in.www1.hephaestus.intelligenceservice --additional-properties useJakartaEe=true,performBeanValidation=true,generateClientAsBean=true,hideGenerationTimestamp=true --package-name de.tum.in.www1.hephaestus.intelligenceservice -o tmp/java-client && shx cp -r tmp/java-client/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice server/application-server/src/main/java/de/tum/in/www1/hephaestus && rimraf tmp", + "generate:api:clients": "npm run generate:api:intelligence-service-client && npm run generate:api:application-server-client", + + "generate:api:application-server": "npm run generate:api:application-server-specs && npm run generate:api:application-server:clean && npm run generate:api:application-server-client", + "generate:api:intelligence-service": "npm run generate:api:intelligence-service:clean && npm run generate:api:intelligence-service-specs && npm run generate:api:intelligence-service-client", + "generate:api": "npm run generate:api:intelligence-service && npm run generate:api:application-server" }, "devDependencies": { "@openapitools/openapi-generator-cli": "2.13.5", - "rimraf": "6.0.1" + "rimraf": "6.0.1", + "shx": "0.3.4" } } diff --git a/server/application-server/pom.xml b/server/application-server/pom.xml index 0aaedd6b..53ba506c 100644 --- a/server/application-server/pom.xml +++ b/server/application-server/pom.xml @@ -34,8 +34,8 @@ - jitpack.io - https://jitpack.io + jitpack.io + https://jitpack.io @@ -47,6 +47,10 @@ org.springframework.boot spring-boot-starter-data-jpa + + org.springframework.boot + spring-boot-starter-oauth2-client + org.springframework.boot spring-boot-starter-oauth2-resource-server @@ -115,6 +119,15 @@ therapi-runtime-javadoc 0.15.0 + + org.springframework.modulith + spring-modulith-starter-core + + + org.springframework.modulith + spring-modulith-starter-test + test + com.github.FelixTJDietrich github-api @@ -130,8 +143,66 @@ jnats 2.20.2 + + org.openapitools + openapi-generator-maven-plugin + 6.6.0 + + + org.openapitools + openapi-generator + 6.6.0 + + + com.squareup.okhttp3 + okhttp + + + com.squareup.okhttp3 + logging-interceptor + + + com.google.code.gson + gson + + + io.gsonfire + gson-fire + 1.8.5 + + + jakarta.ws.rs + jakarta.ws.rs-api + + + jakarta.annotation + jakarta.annotation-api + + + org.openapitools + jackson-databind-nullable + 0.2.6 + + + org.glassfish.jersey.core + jersey-client + + + org.hibernate.validator + hibernate-validator + - + + + + org.springframework.modulith + spring-modulith-bom + 1.2.2 + import + pom + + + @@ -164,7 +235,7 @@ lombok - 500 + 1000 100 @@ -196,5 +267,4 @@ - \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java new file mode 100644 index 00000000..ba4fc629 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/WebConfig.java @@ -0,0 +1,16 @@ +package de.tum.in.www1.hephaestus; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Bean + public RestTemplate restTemplate() { + return new RestTemplate(); + } +} \ No newline at end of file diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ApiClient.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ApiClient.java new file mode 100644 index 00000000..4f663edf --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ApiClient.java @@ -0,0 +1,803 @@ +package de.tum.in.www1.hephaestus.intelligenceservice; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.InvalidMediaTypeException; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.RequestEntity.BodyBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.openapitools.jackson.nullable.JsonNullableModule; + + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.ParseException; +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.TimeZone; +import java.util.function.Supplier; +import java.time.OffsetDateTime; + +import de.tum.in.www1.hephaestus.intelligenceservice.auth.Authentication; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +@Component("de.tum.in.www1.hephaestus.intelligenceservice.ApiClient") +public class ApiClient extends JavaTimeFormatter { + public enum CollectionFormat { + CSV(","), TSV("\t"), SSV(" "), PIPES("|"), MULTI(null); + + private final String separator; + + private CollectionFormat(String separator) { + this.separator = separator; + } + + private String collectionToString(Collection collection) { + return StringUtils.collectionToDelimitedString(collection, separator); + } + } + + private boolean debugging = false; + + private HttpHeaders defaultHeaders = new HttpHeaders(); + private MultiValueMap defaultCookies = new LinkedMultiValueMap(); + + private int maxAttemptsForRetry = 1; + + private long waitTimeMillis = 10; + + private String basePath = "http://localhost"; + + private RestTemplate restTemplate; + + private Map authentications; + + private DateFormat dateFormat; + + public ApiClient() { + this.restTemplate = buildRestTemplate(); + init(); + } + + @Autowired + public ApiClient(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + init(); + } + + protected void init() { + // Use RFC3339 format for date and datetime. + // See http://xml2rfc.ietf.org/public/rfc/html/rfc3339.html#anchor14 + this.dateFormat = new RFC3339DateFormat(); + + // Use UTC as the default time zone. + this.dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + + // Set default User-Agent. + setUserAgent("Java-SDK"); + + // Setup authentications (key: authentication name, value: authentication). + authentications = new HashMap(); + // Prevent the authentications from being modified. + authentications = Collections.unmodifiableMap(authentications); + } + + /** + * Get the current base path + * + * @return String the base path + */ + public String getBasePath() { + return basePath; + } + + /** + * Set the base path, which should include the host + * + * @param basePath the base path + * @return ApiClient this client + */ + public ApiClient setBasePath(String basePath) { + this.basePath = basePath; + return this; + } + + /** + * Get the max attempts for retry + * + * @return int the max attempts + */ + public int getMaxAttemptsForRetry() { + return maxAttemptsForRetry; + } + + /** + * Set the max attempts for retry + * + * @param maxAttemptsForRetry the max attempts for retry + * @return ApiClient this client + */ + public ApiClient setMaxAttemptsForRetry(int maxAttemptsForRetry) { + this.maxAttemptsForRetry = maxAttemptsForRetry; + return this; + } + + /** + * Get the wait time in milliseconds + * + * @return long wait time in milliseconds + */ + public long getWaitTimeMillis() { + return waitTimeMillis; + } + + /** + * Set the wait time in milliseconds + * + * @param waitTimeMillis the wait time in milliseconds + * @return ApiClient this client + */ + public ApiClient setWaitTimeMillis(long waitTimeMillis) { + this.waitTimeMillis = waitTimeMillis; + return this; + } + + /** + * Get authentications (key: authentication name, value: authentication). + * + * @return Map the currently configured authentication types + */ + public Map getAuthentications() { + return authentications; + } + + /** + * Get authentication for the given name. + * + * @param authName The authentication name + * @return The authentication, null if not found + */ + public Authentication getAuthentication(String authName) { + return authentications.get(authName); + } + + + + + + /** + * Set the User-Agent header's value (by adding to the default header map). + * + * @param userAgent the user agent string + * @return ApiClient this client + */ + public ApiClient setUserAgent(String userAgent) { + addDefaultHeader("User-Agent", userAgent); + return this; + } + + /** + * Add a default header. + * + * @param name The header's name + * @param value The header's value + * @return ApiClient this client + */ + public ApiClient addDefaultHeader(String name, String value) { + if (defaultHeaders.containsKey(name)) { + defaultHeaders.remove(name); + } + defaultHeaders.add(name, value); + return this; + } + + /** + * Add a default cookie. + * + * @param name The cookie's name + * @param value The cookie's value + * @return ApiClient this client + */ + public ApiClient addDefaultCookie(String name, String value) { + if (defaultCookies.containsKey(name)) { + defaultCookies.remove(name); + } + defaultCookies.add(name, value); + return this; + } + + public void setDebugging(boolean debugging) { + List currentInterceptors = this.restTemplate.getInterceptors(); + if (debugging) { + if (currentInterceptors == null) { + currentInterceptors = new ArrayList(); + } + ClientHttpRequestInterceptor interceptor = new ApiClientHttpRequestInterceptor(); + currentInterceptors.add(interceptor); + this.restTemplate.setInterceptors(currentInterceptors); + } else { + if (currentInterceptors != null && !currentInterceptors.isEmpty()) { + Iterator iter = currentInterceptors.iterator(); + while (iter.hasNext()) { + ClientHttpRequestInterceptor interceptor = iter.next(); + if (interceptor instanceof ApiClientHttpRequestInterceptor) { + iter.remove(); + } + } + this.restTemplate.setInterceptors(currentInterceptors); + } + } + this.debugging = debugging; + } + + /** + * Check that whether debugging is enabled for this API client. + * @return boolean true if this client is enabled for debugging, false otherwise + */ + public boolean isDebugging() { + return debugging; + } + + /** + * Get the date format used to parse/format date parameters. + * @return DateFormat format + */ + public DateFormat getDateFormat() { + return dateFormat; + } + + /** + * Set the date format used to parse/format date parameters. + * @param dateFormat Date format + * @return API client + */ + public ApiClient setDateFormat(DateFormat dateFormat) { + this.dateFormat = dateFormat; + return this; + } + + /** + * Parse the given string into Date object. + * + * @param str the string to parse + * @return the Date parsed from the string + */ + public Date parseDate(String str) { + try { + return dateFormat.parse(str); + } catch (ParseException e) { + throw new RuntimeException(e); + } + } + + /** + * Format the given Date object into string. + * + * @param date the date to format + * @return the formatted date as string + */ + public String formatDate(Date date) { + return dateFormat.format(date); + } + + /** + * Format the given parameter object into string. + * + * @param param the object to convert + * @return String the parameter represented as a String + */ + public String parameterToString(Object param) { + if (param == null) { + return ""; + } else if (param instanceof Date) { + return formatDate( (Date) param); + } else if (param instanceof OffsetDateTime) { + return formatOffsetDateTime((OffsetDateTime) param); + } else if (param instanceof Collection) { + StringBuilder b = new StringBuilder(); + for (Object o : (Collection) param) { + if (b.length() > 0) { + b.append(","); + } + b.append(String.valueOf(o)); + } + return b.toString(); + } else { + return String.valueOf(param); + } + } + + /** + * Formats the specified collection path parameter to a string value. + * + * @param collectionFormat The collection format of the parameter. + * @param values The values of the parameter. + * @return String representation of the parameter + */ + public String collectionPathParameterToString(CollectionFormat collectionFormat, Collection values) { + // create the value based on the collection format + if (CollectionFormat.MULTI.equals(collectionFormat)) { + // not valid for path params + return parameterToString(values); + } + + // collectionFormat is assumed to be "csv" by default + if (collectionFormat == null) { + collectionFormat = CollectionFormat.CSV; + } + + return collectionFormat.collectionToString(values); + } + + /** + * Converts a parameter to a {@link MultiValueMap} for use in REST requests + * + * @param collectionFormat The format to convert to + * @param name The name of the parameter + * @param value The parameter's value + * @return a Map containing the String value(s) of the input parameter + */ + public MultiValueMap parameterToMultiValueMap(CollectionFormat collectionFormat, String name, Object value) { + final MultiValueMap params = new LinkedMultiValueMap(); + + if (name == null || name.isEmpty() || value == null) { + return params; + } + + if (collectionFormat == null) { + collectionFormat = CollectionFormat.CSV; + } + + if (value instanceof Map) { + @SuppressWarnings("unchecked") + final Map valuesMap = (Map) value; + for (final Entry entry : valuesMap.entrySet()) { + params.add(entry.getKey(), parameterToString(entry.getValue())); + } + return params; + } + + Collection valueCollection = null; + if (value instanceof Collection) { + valueCollection = (Collection) value; + } else { + params.add(name, parameterToString(value)); + return params; + } + + if (valueCollection.isEmpty()) { + return params; + } + + if (collectionFormat.equals(CollectionFormat.MULTI)) { + for (Object item : valueCollection) { + params.add(name, parameterToString(item)); + } + return params; + } + + List values = new ArrayList(); + for (Object o : valueCollection) { + values.add(parameterToString(o)); + } + params.add(name, collectionFormat.collectionToString(values)); + + return params; + } + + /** + * Check if the given {@code String} is a JSON MIME. + * + * @param mediaType the input MediaType + * @return boolean true if the MediaType represents JSON, false otherwise + */ + public boolean isJsonMime(String mediaType) { + // "* / *" is default to JSON + if ("*/*".equals(mediaType)) { + return true; + } + + try { + return isJsonMime(MediaType.parseMediaType(mediaType)); + } catch (InvalidMediaTypeException e) { + } + return false; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * + * @param mediaType the input MediaType + * @return boolean true if the MediaType represents JSON, false otherwise + */ + public boolean isJsonMime(MediaType mediaType) { + return mediaType != null && (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || mediaType.getSubtype().matches("^.*\\+json[;]?\\s*$")); + } + + /** + * Check if the given {@code String} is a Problem JSON MIME (RFC-7807). + * + * @param mediaType the input MediaType + * @return boolean true if the MediaType represents Problem JSON, false otherwise + */ + public boolean isProblemJsonMime(String mediaType) { + return "application/problem+json".equalsIgnoreCase(mediaType); + } + + /** + * Select the Accept header's value from the given accepts array: + * if JSON exists in the given array, use it; + * otherwise use all of them (joining into a string) + * + * @param accepts The accepts array to select from + * @return List The list of MediaTypes to use for the Accept header + */ + public List selectHeaderAccept(String[] accepts) { + if (accepts.length == 0) { + return null; + } + for (String accept : accepts) { + MediaType mediaType = MediaType.parseMediaType(accept); + if (isJsonMime(mediaType) && !isProblemJsonMime(accept)) { + return Collections.singletonList(mediaType); + } + } + return MediaType.parseMediaTypes(StringUtils.arrayToCommaDelimitedString(accepts)); + } + + /** + * Select the Content-Type header's value from the given array: + * if JSON exists in the given array, use it; + * otherwise use the first one of the array. + * + * @param contentTypes The Content-Type array to select from + * @return MediaType The Content-Type header to use. If the given array is empty, JSON will be used. + */ + public MediaType selectHeaderContentType(String[] contentTypes) { + if (contentTypes.length == 0) { + return MediaType.APPLICATION_JSON; + } + for (String contentType : contentTypes) { + MediaType mediaType = MediaType.parseMediaType(contentType); + if (isJsonMime(mediaType)) { + return mediaType; + } + } + return MediaType.parseMediaType(contentTypes[0]); + } + + /** + * Select the body to use for the request + * + * @param obj the body object + * @param formParams the form parameters + * @param contentType the content type of the request + * @return Object the selected body + */ + protected Object selectBody(Object obj, MultiValueMap formParams, MediaType contentType) { + boolean isForm = MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType); + return isForm ? formParams : obj; + } + + /** + * Expand path template with variables + * + * @param pathTemplate path template with placeholders + * @param variables variables to replace + * @return path with placeholders replaced by variables + */ + public String expandPath(String pathTemplate, Map variables) { + return restTemplate.getUriTemplateHandler().expand(pathTemplate, variables).toString(); + } + + /** + * Include queryParams in uriParams taking into account the paramName + * + * @param queryParams The query parameters + * @param uriParams The path parameters + * return templatized query string + */ + public String generateQueryUri(MultiValueMap queryParams, Map uriParams) { + StringBuilder queryBuilder = new StringBuilder(); + queryParams.forEach((name, values) -> { + try { + final String encodedName = URLEncoder.encode(name.toString(), "UTF-8"); + if (CollectionUtils.isEmpty(values)) { + if (queryBuilder.length() != 0) { + queryBuilder.append('&'); + } + queryBuilder.append(encodedName); + } else { + int valueItemCounter = 0; + for (Object value : values) { + if (queryBuilder.length() != 0) { + queryBuilder.append('&'); + } + queryBuilder.append(encodedName); + if (value != null) { + String templatizedKey = encodedName + valueItemCounter++; + uriParams.put(templatizedKey, value.toString()); + queryBuilder.append('=').append("{").append(templatizedKey).append("}"); + } + } + } + } catch (UnsupportedEncodingException e) { + + } + }); + return queryBuilder.toString(); + + } + + /** + * Invoke API by sending HTTP request with the given options. + * + * @param the return type to use + * @param path The sub-path of the HTTP URL + * @param method The request method + * @param pathParams The path parameters + * @param queryParams The query parameters + * @param body The request body object + * @param headerParams The header parameters + * @param cookieParams The cookie parameters + * @param formParams The form parameters + * @param accept The request's Accept header + * @param contentType The request's Content-Type header + * @param authNames The authentications to apply + * @param returnType The return type into which to deserialize the response + * @return ResponseEntity<T> The response of the chosen type + */ + public ResponseEntity invokeAPI(String path, HttpMethod method, Map pathParams, MultiValueMap queryParams, Object body, HttpHeaders headerParams, MultiValueMap cookieParams, MultiValueMap formParams, List accept, MediaType contentType, String[] authNames, ParameterizedTypeReference returnType) throws RestClientException { + updateParamsForAuth(authNames, queryParams, headerParams, cookieParams); + + Map uriParams = new HashMap<>(); + uriParams.putAll(pathParams); + + String finalUri = path; + + if (queryParams != null && !queryParams.isEmpty()) { + //Include queryParams in uriParams taking into account the paramName + String queryUri = generateQueryUri(queryParams, uriParams); + //Append to finalUri the templatized query string like "?param1={param1Value}&....... + finalUri += "?" + queryUri; + } + String expandedPath = this.expandPath(finalUri, uriParams); + final UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(basePath).path(expandedPath); + + URI uri; + try { + uri = new URI(builder.build().toUriString()); + } catch (URISyntaxException ex) { + throw new RestClientException("Could not build URL: " + builder.toUriString(), ex); + } + + final BodyBuilder requestBuilder = RequestEntity.method(method, UriComponentsBuilder.fromHttpUrl(basePath).toUriString() + finalUri, uriParams); + if (accept != null) { + requestBuilder.accept(accept.toArray(new MediaType[accept.size()])); + } + if (contentType != null) { + requestBuilder.contentType(contentType); + } + + addHeadersToRequest(headerParams, requestBuilder); + addHeadersToRequest(defaultHeaders, requestBuilder); + addCookiesToRequest(cookieParams, requestBuilder); + addCookiesToRequest(defaultCookies, requestBuilder); + + RequestEntity requestEntity = requestBuilder.body(selectBody(body, formParams, contentType)); + + ResponseEntity responseEntity = null; + int attempts = 0; + while (attempts < maxAttemptsForRetry) { + try { + responseEntity = restTemplate.exchange(requestEntity, returnType); + break; + } catch (HttpServerErrorException | HttpClientErrorException ex) { + if (ex instanceof HttpServerErrorException + || ((HttpClientErrorException) ex) + .getStatusCode() + .equals(HttpStatus.TOO_MANY_REQUESTS)) { + attempts++; + if (attempts < maxAttemptsForRetry) { + try { + Thread.sleep(waitTimeMillis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } else { + throw ex; + } + } else { + throw ex; + } + } + } + + if (responseEntity == null) { + throw new RestClientException("ResponseEntity is null"); + } + + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity; + } else { + // The error handler built into the RestTemplate should handle 400 and 500 series errors. + throw new RestClientException("API returned " + responseEntity.getStatusCode() + " and it wasn't handled by the RestTemplate error handler"); + } + } + + /** + * Add headers to the request that is being built + * @param headers The headers to add + * @param requestBuilder The current request + */ + protected void addHeadersToRequest(HttpHeaders headers, BodyBuilder requestBuilder) { + for (Entry> entry : headers.entrySet()) { + List values = entry.getValue(); + for (String value : values) { + if (value != null) { + requestBuilder.header(entry.getKey(), value); + } + } + } + } + + /** + * Add cookies to the request that is being built + * + * @param cookies The cookies to add + * @param requestBuilder The current request + */ + protected void addCookiesToRequest(MultiValueMap cookies, BodyBuilder requestBuilder) { + if (!cookies.isEmpty()) { + requestBuilder.header("Cookie", buildCookieHeader(cookies)); + } + } + + /** + * Build cookie header. Keeps a single value per cookie (as per + * RFC6265 section 5.3). + * + * @param cookies map all cookies + * @return header string for cookies. + */ + private String buildCookieHeader(MultiValueMap cookies) { + final StringBuilder cookieValue = new StringBuilder(); + String delimiter = ""; + for (final Map.Entry> entry : cookies.entrySet()) { + final String value = entry.getValue().get(entry.getValue().size() - 1); + cookieValue.append(String.format("%s%s=%s", delimiter, entry.getKey(), value)); + delimiter = "; "; + } + return cookieValue.toString(); + } + + /** + * Build the RestTemplate used to make HTTP requests. + * @return RestTemplate + */ + protected RestTemplate buildRestTemplate() { + RestTemplate restTemplate = new RestTemplate(); + // This allows us to read the response more than once - Necessary for debugging. + restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(restTemplate.getRequestFactory())); + + // disable default URL encoding + DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(); + uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY); + restTemplate.setUriTemplateHandler(uriBuilderFactory); + return restTemplate; + } + + /** + * Update query and header parameters based on authentication settings. + * + * @param authNames The authentications to apply + * @param queryParams The query parameters + * @param headerParams The header parameters + */ + protected void updateParamsForAuth(String[] authNames, MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams) { + for (String authName : authNames) { + Authentication auth = authentications.get(authName); + if (auth == null) { + throw new RestClientException("Authentication undefined: " + authName); + } + auth.applyToParams(queryParams, headerParams, cookieParams); + } + } + + private class ApiClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + private final Log log = LogFactory.getLog(ApiClientHttpRequestInterceptor.class); + + @Override + public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { + logRequest(request, body); + ClientHttpResponse response = execution.execute(request, body); + logResponse(response); + return response; + } + + private void logRequest(HttpRequest request, byte[] body) throws UnsupportedEncodingException { + log.info("URI: " + request.getURI()); + log.info("HTTP Method: " + request.getMethod()); + log.info("HTTP Headers: " + headersToString(request.getHeaders())); + log.info("Request Body: " + new String(body, StandardCharsets.UTF_8)); + } + + private void logResponse(ClientHttpResponse response) throws IOException { + log.info("HTTP Status Code: " + response.getStatusCode().value()); + log.info("Status Text: " + response.getStatusText()); + log.info("HTTP Headers: " + headersToString(response.getHeaders())); + log.info("Response Body: " + bodyToString(response.getBody())); + } + + private String headersToString(HttpHeaders headers) { + if(headers == null || headers.isEmpty()) { + return ""; + } + StringBuilder builder = new StringBuilder(); + for (Entry> entry : headers.entrySet()) { + builder.append(entry.getKey()).append("=["); + for (String value : entry.getValue()) { + builder.append(value).append(","); + } + builder.setLength(builder.length() - 1); // Get rid of trailing comma + builder.append("],"); + } + builder.setLength(builder.length() - 1); // Get rid of trailing comma + return builder.toString(); + } + + private String bodyToString(InputStream body) throws IOException { + StringBuilder builder = new StringBuilder(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8)); + String line = bufferedReader.readLine(); + while (line != null) { + builder.append(line).append(System.lineSeparator()); + line = bufferedReader.readLine(); + } + bufferedReader.close(); + return builder.toString(); + } + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BaseApi.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BaseApi.java new file mode 100644 index 00000000..2c4b0c55 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BaseApi.java @@ -0,0 +1,74 @@ +package de.tum.in.www1.hephaestus.intelligenceservice; + +import org.springframework.web.client.RestClientException; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public abstract class BaseApi { + + protected ApiClient apiClient; + + public BaseApi() { + this(new ApiClient()); + } + + public BaseApi(ApiClient apiClient) { + this.apiClient = apiClient; + } + + public ApiClient getApiClient() { + return apiClient; + } + + public void setApiClient(ApiClient apiClient) { + this.apiClient = apiClient; + } + + /** + * Directly invoke the API for the given URL. Useful if the API returns direct links/URLs for subsequent requests. + * @param url The URL for the request, either full URL or only the path. + * @param method The HTTP method for the request. + * @return ResponseEntity<Void> + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ResponseEntity invokeAPI(String url, HttpMethod method) throws RestClientException { + return invokeAPI(url, method, null, new ParameterizedTypeReference() {}); + } + + /** + * Directly invoke the API for the given URL. Useful if the API returns direct links/URLs for subsequent requests. + * @param url The URL for the request, either full URL or only the path. + * @param method The HTTP method for the request. + * @param request The request object. + * @return ResponseEntity<Void> + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ResponseEntity invokeAPI(String url, HttpMethod method, Object request) throws RestClientException { + return invokeAPI(url, method, request, new ParameterizedTypeReference() {}); + } + + /** + * Directly invoke the API for the given URL. Useful if the API returns direct links/URLs for subsequent requests. + * @param url The URL for the request, either full URL or only the path. + * @param method The HTTP method for the request. + * @param returnType The return type. + * @return ResponseEntity in the specified type. + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ResponseEntity invokeAPI(String url, HttpMethod method, ParameterizedTypeReference returnType) throws RestClientException { + return invokeAPI(url, method, null, returnType); + } + + /** + * Directly invoke the API for the given URL. Useful if the API returns direct links/URLs for subsequent requests. + * @param url The URL for the request, either full URL or only the path. + * @param method The HTTP method for the request. + * @param request The request object. + * @param returnType The return type. + * @return ResponseEntity in the specified type. + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public abstract ResponseEntity invokeAPI(String url, HttpMethod method, Object request, ParameterizedTypeReference returnType) throws RestClientException; +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BeanValidationException.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BeanValidationException.java new file mode 100644 index 00000000..b80b521c --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/BeanValidationException.java @@ -0,0 +1,27 @@ +package de.tum.in.www1.hephaestus.intelligenceservice; + +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ValidationException; + +public class BeanValidationException extends ValidationException { + /** + * + */ + private static final long serialVersionUID = -5294733947409491364L; + Set> violations; + + public BeanValidationException(Set> violations) { + this.violations = violations; + } + + public Set> getViolations() { + return violations; + } + + public void setViolations(Set> violations) { + this.violations = violations; + } + +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/JavaTimeFormatter.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/JavaTimeFormatter.java new file mode 100644 index 00000000..504d89cb --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/JavaTimeFormatter.java @@ -0,0 +1,64 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package de.tum.in.www1.hephaestus.intelligenceservice; + +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Class that add parsing/formatting support for Java 8+ {@code OffsetDateTime} class. + * It's generated for java clients when {@code AbstractJavaCodegen#dateLibrary} specified as {@code java8}. + */ +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class JavaTimeFormatter { + + private DateTimeFormatter offsetDateTimeFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; + + /** + * Get the date format used to parse/format {@code OffsetDateTime} parameters. + * @return DateTimeFormatter + */ + public DateTimeFormatter getOffsetDateTimeFormatter() { + return offsetDateTimeFormatter; + } + + /** + * Set the date format used to parse/format {@code OffsetDateTime} parameters. + * @param offsetDateTimeFormatter {@code DateTimeFormatter} + */ + public void setOffsetDateTimeFormatter(DateTimeFormatter offsetDateTimeFormatter) { + this.offsetDateTimeFormatter = offsetDateTimeFormatter; + } + + /** + * Parse the given string into {@code OffsetDateTime} object. + * @param str String + * @return {@code OffsetDateTime} + */ + public OffsetDateTime parseOffsetDateTime(String str) { + try { + return OffsetDateTime.parse(str, offsetDateTimeFormatter); + } catch (DateTimeParseException e) { + throw new RuntimeException(e); + } + } + /** + * Format the given {@code OffsetDateTime} object into string. + * @param offsetDateTime {@code OffsetDateTime} + * @return {@code OffsetDateTime} in string format + */ + public String formatOffsetDateTime(OffsetDateTime offsetDateTime) { + return offsetDateTimeFormatter.format(offsetDateTime); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/RFC3339DateFormat.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/RFC3339DateFormat.java new file mode 100644 index 00000000..cd4ed44b --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/RFC3339DateFormat.java @@ -0,0 +1,58 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + +package de.tum.in.www1.hephaestus.intelligenceservice; + +import com.fasterxml.jackson.databind.util.StdDateFormat; + +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.util.Date; +import java.text.DecimalFormat; +import java.util.GregorianCalendar; +import java.util.TimeZone; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class RFC3339DateFormat extends DateFormat { + private static final long serialVersionUID = 1L; + private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC"); + + private final StdDateFormat fmt = new StdDateFormat() + .withTimeZone(TIMEZONE_Z) + .withColonInTimeZone(true); + + public RFC3339DateFormat() { + this.calendar = new GregorianCalendar(); + this.numberFormat = new DecimalFormat(); + } + + @Override + public Date parse(String source) { + return parse(source, new ParsePosition(0)); + } + + @Override + public Date parse(String source, ParsePosition pos) { + return fmt.parse(source, pos); + } + + @Override + public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { + return fmt.format(date, toAppendTo, fieldPosition); + } + + @Override + public Object clone() { + return super.clone(); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerConfiguration.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerConfiguration.java new file mode 100644 index 00000000..6c028d27 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerConfiguration.java @@ -0,0 +1,59 @@ +package de.tum.in.www1.hephaestus.intelligenceservice; + +import java.util.Map; + +/** + * Representing a Server configuration. + */ +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ServerConfiguration { + public String URL; + public String description; + public Map variables; + + /** + * @param URL A URL to the target host. + * @param description A description of the host designated by the URL. + * @param variables A map between a variable name and its value. The value is used for substitution in the server's URL template. + */ + public ServerConfiguration(String URL, String description, Map variables) { + this.URL = URL; + this.description = description; + this.variables = variables; + } + + /** + * Format URL template using given variables. + * + * @param variables A map between a variable name and its value. + * @return Formatted URL. + */ + public String URL(Map variables) { + String url = this.URL; + + // go through variables and replace placeholders + for (Map.Entry variable: this.variables.entrySet()) { + String name = variable.getKey(); + ServerVariable serverVariable = variable.getValue(); + String value = serverVariable.defaultValue; + + if (variables != null && variables.containsKey(name)) { + value = variables.get(name); + if (serverVariable.enumValues.size() > 0 && !serverVariable.enumValues.contains(value)) { + throw new IllegalArgumentException("The variable " + name + " in the server URL has invalid value " + value + "."); + } + } + url = url.replace("{" + name + "}", value); + } + return url; + } + + /** + * Format URL template using default server variables. + * + * @return Formatted URL. + */ + public String URL() { + return URL(null); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerVariable.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerVariable.java new file mode 100644 index 00000000..dfffb5c9 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/ServerVariable.java @@ -0,0 +1,24 @@ +package de.tum.in.www1.hephaestus.intelligenceservice; + +import java.util.HashSet; + +/** + * Representing a Server Variable for server URL template substitution. + */ +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ServerVariable { + public String description; + public String defaultValue; + public HashSet enumValues = null; + + /** + * @param description A description for the server variable. + * @param defaultValue The default value to use for substitution. + * @param enumValues An enumeration of string values to be used if the substitution options are from a limited set. + */ + public ServerVariable(String description, String defaultValue, HashSet enumValues) { + this.description = description; + this.defaultValue = defaultValue; + this.enumValues = enumValues; + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/api/DefaultApi.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/api/DefaultApi.java new file mode 100644 index 00000000..0158bbd6 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/api/DefaultApi.java @@ -0,0 +1,119 @@ +package de.tum.in.www1.hephaestus.intelligenceservice.api; + +import de.tum.in.www1.hephaestus.intelligenceservice.ApiClient; +import de.tum.in.www1.hephaestus.intelligenceservice.BaseApi; + +import de.tum.in.www1.hephaestus.intelligenceservice.model.ChatRequest; +import de.tum.in.www1.hephaestus.intelligenceservice.model.ChatResponse; +import de.tum.in.www1.hephaestus.intelligenceservice.model.HTTPValidationError; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.io.FileSystemResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +@Component("de.tum.in.www1.hephaestus.intelligenceservice.api.DefaultApi") +public class DefaultApi extends BaseApi { + + public DefaultApi() { + super(new ApiClient()); + } + + @Autowired + public DefaultApi(ApiClient apiClient) { + super(apiClient); + } + + /** + * Get a response from an LLM to a chat message. + * + *

200 - Successful Response + *

422 - Validation Error + * @param chatRequest (required) + * @return ChatResponse + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ChatResponse chatChatPost(ChatRequest chatRequest) throws RestClientException { + return chatChatPostWithHttpInfo(chatRequest).getBody(); + } + + /** + * Get a response from an LLM to a chat message. + * + *

200 - Successful Response + *

422 - Validation Error + * @param chatRequest (required) + * @return ResponseEntity<ChatResponse> + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ResponseEntity chatChatPostWithHttpInfo(ChatRequest chatRequest) throws RestClientException { + Object localVarPostBody = chatRequest; + + // verify the required parameter 'chatRequest' is set + if (chatRequest == null) { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Missing the required parameter 'chatRequest' when calling chatChatPost"); + } + + + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "application/json" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] { }; + + ParameterizedTypeReference localReturnType = new ParameterizedTypeReference() {}; + return apiClient.invokeAPI("/chat", HttpMethod.POST, Collections.emptyMap(), localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, localReturnType); + } + + @Override + public ResponseEntity invokeAPI(String url, HttpMethod method, Object request, ParameterizedTypeReference returnType) throws RestClientException { + String localVarPath = url.replace(apiClient.getBasePath(), ""); + Object localVarPostBody = request; + + final Map uriVariables = new HashMap(); + final MultiValueMap localVarQueryParams = new LinkedMultiValueMap(); + final HttpHeaders localVarHeaderParams = new HttpHeaders(); + final MultiValueMap localVarCookieParams = new LinkedMultiValueMap(); + final MultiValueMap localVarFormParams = new LinkedMultiValueMap(); + + final String[] localVarAccepts = { + "application/json" + }; + final List localVarAccept = apiClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = { + "application/json" + }; + final MediaType localVarContentType = apiClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] { }; + + return apiClient.invokeAPI(localVarPath, method, uriVariables, localVarQueryParams, localVarPostBody, localVarHeaderParams, localVarCookieParams, localVarFormParams, localVarAccept, localVarContentType, localVarAuthNames, returnType); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/ApiKeyAuth.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/ApiKeyAuth.java new file mode 100644 index 00000000..f6f61277 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/ApiKeyAuth.java @@ -0,0 +1,62 @@ +package de.tum.in.www1.hephaestus.intelligenceservice.auth; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.MultiValueMap; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ApiKeyAuth implements Authentication { + private final String location; + private final String paramName; + + private String apiKey; + private String apiKeyPrefix; + + public ApiKeyAuth(String location, String paramName) { + this.location = location; + this.paramName = paramName; + } + + public String getLocation() { + return location; + } + + public String getParamName() { + return paramName; + } + + public String getApiKey() { + return apiKey; + } + + public void setApiKey(String apiKey) { + this.apiKey = apiKey; + } + + public String getApiKeyPrefix() { + return apiKeyPrefix; + } + + public void setApiKeyPrefix(String apiKeyPrefix) { + this.apiKeyPrefix = apiKeyPrefix; + } + + @Override + public void applyToParams(MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams) { + if (apiKey == null) { + return; + } + String value; + if (apiKeyPrefix != null) { + value = apiKeyPrefix + " " + apiKey; + } else { + value = apiKey; + } + if (location.equals("query")) { + queryParams.add(paramName, value); + } else if (location.equals("header")) { + headerParams.add(paramName, value); + } else if (location.equals("cookie")) { + cookieParams.add(paramName, value); + } + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/Authentication.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/Authentication.java new file mode 100644 index 00000000..0840ad65 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/Authentication.java @@ -0,0 +1,14 @@ +package de.tum.in.www1.hephaestus.intelligenceservice.auth; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.MultiValueMap; + +public interface Authentication { + /** + * Apply authentication settings to header and / or query parameters. + * @param queryParams The query parameters for the request + * @param headerParams The header parameters for the request + * @param cookieParams The cookie parameters for the request + */ + public void applyToParams(MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams); +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBasicAuth.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBasicAuth.java new file mode 100644 index 00000000..f7cb4e0f --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBasicAuth.java @@ -0,0 +1,38 @@ +package de.tum.in.www1.hephaestus.intelligenceservice.auth; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.MultiValueMap; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class HttpBasicAuth implements Authentication { + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public void applyToParams(MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams) { + if (username == null && password == null) { + return; + } + String str = (username == null ? "" : username) + ":" + (password == null ? "" : password); + headerParams.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64.getEncoder().encodeToString(str.getBytes(StandardCharsets.UTF_8))); + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBearerAuth.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBearerAuth.java new file mode 100644 index 00000000..6cf59a22 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/auth/HttpBearerAuth.java @@ -0,0 +1,56 @@ +package de.tum.in.www1.hephaestus.intelligenceservice.auth; + +import java.util.Optional; +import java.util.function.Supplier; +import org.springframework.http.HttpHeaders; +import org.springframework.util.MultiValueMap; + +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class HttpBearerAuth implements Authentication { + private final String scheme; + private Supplier tokenSupplier; + + public HttpBearerAuth(String scheme) { + this.scheme = scheme; + } + + /** + * Gets the token, which together with the scheme, will be sent as the value of the Authorization header. + * + * @return The bearer token + */ + public String getBearerToken() { + return tokenSupplier.get(); + } + + /** + * Sets the token, which together with the scheme, will be sent as the value of the Authorization header. + * + * @param bearerToken The bearer token to send in the Authorization header + */ + public void setBearerToken(String bearerToken) { + this.tokenSupplier = () -> bearerToken; + } + + /** + * Sets the supplier of tokens, which together with the scheme, will be sent as the value of the Authorization header. + * + * @param tokenSupplier The supplier of bearer tokens to send in the Authorization header + */ + public void setBearerToken(Supplier tokenSupplier) { + this.tokenSupplier = tokenSupplier; + } + + @Override + public void applyToParams(MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams) { + String bearerToken = Optional.ofNullable(tokenSupplier).map(Supplier::get).orElse(null); + if (bearerToken == null) { + return; + } + headerParams.add(HttpHeaders.AUTHORIZATION, (scheme != null ? upperCaseBearer(scheme) + " " : "") + bearerToken); + } + + private static String upperCaseBearer(String scheme) { + return ("bearer".equalsIgnoreCase(scheme)) ? "Bearer" : scheme; + } +} diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatRequest.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatRequest.java new file mode 100644 index 00000000..70234d22 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatRequest.java @@ -0,0 +1,104 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package de.tum.in.www1.hephaestus.intelligenceservice.model; + +import java.util.Objects; +import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.hibernate.validator.constraints.*; + +/** + * ChatRequest + */ +@JsonPropertyOrder({ + ChatRequest.JSON_PROPERTY_MESSAGE +}) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ChatRequest { + public static final String JSON_PROPERTY_MESSAGE = "message"; + private String message; + + public ChatRequest() { + } + + public ChatRequest message(String message) { + + this.message = message; + return this; + } + + /** + * Get message + * @return message + */ + @jakarta.annotation.Nonnull + @JsonProperty(JSON_PROPERTY_MESSAGE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getMessage() { + return message; + } + + + @JsonProperty(JSON_PROPERTY_MESSAGE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setMessage(String message) { + this.message = message; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChatRequest chatRequest = (ChatRequest) o; + return Objects.equals(this.message, chatRequest.message); + } + + @Override + public int hashCode() { + return Objects.hash(message); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ChatRequest {\n"); + sb.append(" message: ").append(toIndentedString(message)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatResponse.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatResponse.java new file mode 100644 index 00000000..a601104d --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ChatResponse.java @@ -0,0 +1,104 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package de.tum.in.www1.hephaestus.intelligenceservice.model; + +import java.util.Objects; +import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.hibernate.validator.constraints.*; + +/** + * ChatResponse + */ +@JsonPropertyOrder({ + ChatResponse.JSON_PROPERTY_RESPONSE +}) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ChatResponse { + public static final String JSON_PROPERTY_RESPONSE = "response"; + private String response; + + public ChatResponse() { + } + + public ChatResponse response(String response) { + + this.response = response; + return this; + } + + /** + * Get response + * @return response + */ + @jakarta.annotation.Nonnull + @JsonProperty(JSON_PROPERTY_RESPONSE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getResponse() { + return response; + } + + + @JsonProperty(JSON_PROPERTY_RESPONSE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setResponse(String response) { + this.response = response; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ChatResponse chatResponse = (ChatResponse) o; + return Objects.equals(this.response, chatResponse.response); + } + + @Override + public int hashCode() { + return Objects.hash(response); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ChatResponse {\n"); + sb.append(" response: ").append(toIndentedString(response)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/HTTPValidationError.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/HTTPValidationError.java new file mode 100644 index 00000000..c66ebc68 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/HTTPValidationError.java @@ -0,0 +1,116 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package de.tum.in.www1.hephaestus.intelligenceservice.model; + +import java.util.Objects; +import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import de.tum.in.www1.hephaestus.intelligenceservice.model.ValidationError; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.hibernate.validator.constraints.*; + +/** + * HTTPValidationError + */ +@JsonPropertyOrder({ + HTTPValidationError.JSON_PROPERTY_DETAIL +}) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class HTTPValidationError { + public static final String JSON_PROPERTY_DETAIL = "detail"; + private List detail = new ArrayList<>(); + + public HTTPValidationError() { + } + + public HTTPValidationError detail(List detail) { + + this.detail = detail; + return this; + } + + public HTTPValidationError addDetailItem(ValidationError detailItem) { + if (this.detail == null) { + this.detail = new ArrayList<>(); + } + this.detail.add(detailItem); + return this; + } + + /** + * Get detail + * @return detail + */ + @jakarta.annotation.Nullable + @JsonProperty(JSON_PROPERTY_DETAIL) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + + public List getDetail() { + return detail; + } + + + @JsonProperty(JSON_PROPERTY_DETAIL) + @JsonInclude(value = JsonInclude.Include.USE_DEFAULTS) + public void setDetail(List detail) { + this.detail = detail; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + HTTPValidationError htTPValidationError = (HTTPValidationError) o; + return Objects.equals(this.detail, htTPValidationError.detail); + } + + @Override + public int hashCode() { + return Objects.hash(detail); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class HTTPValidationError {\n"); + sb.append(" detail: ").append(toIndentedString(detail)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationError.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationError.java new file mode 100644 index 00000000..458a22e0 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationError.java @@ -0,0 +1,178 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package de.tum.in.www1.hephaestus.intelligenceservice.model; + +import java.util.Objects; +import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonValue; +import de.tum.in.www1.hephaestus.intelligenceservice.model.ValidationErrorLocInner; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.hibernate.validator.constraints.*; + +/** + * ValidationError + */ +@JsonPropertyOrder({ + ValidationError.JSON_PROPERTY_LOC, + ValidationError.JSON_PROPERTY_MSG, + ValidationError.JSON_PROPERTY_TYPE +}) +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ValidationError { + public static final String JSON_PROPERTY_LOC = "loc"; + private List loc = new ArrayList<>(); + + public static final String JSON_PROPERTY_MSG = "msg"; + private String msg; + + public static final String JSON_PROPERTY_TYPE = "type"; + private String type; + + public ValidationError() { + } + + public ValidationError loc(List loc) { + + this.loc = loc; + return this; + } + + public ValidationError addLocItem(ValidationErrorLocInner locItem) { + if (this.loc == null) { + this.loc = new ArrayList<>(); + } + this.loc.add(locItem); + return this; + } + + /** + * Get loc + * @return loc + */ + @jakarta.annotation.Nonnull + @JsonProperty(JSON_PROPERTY_LOC) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public List getLoc() { + return loc; + } + + + @JsonProperty(JSON_PROPERTY_LOC) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setLoc(List loc) { + this.loc = loc; + } + + public ValidationError msg(String msg) { + + this.msg = msg; + return this; + } + + /** + * Get msg + * @return msg + */ + @jakarta.annotation.Nonnull + @JsonProperty(JSON_PROPERTY_MSG) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getMsg() { + return msg; + } + + + @JsonProperty(JSON_PROPERTY_MSG) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setMsg(String msg) { + this.msg = msg; + } + + public ValidationError type(String type) { + + this.type = type; + return this; + } + + /** + * Get type + * @return type + */ + @jakarta.annotation.Nonnull + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + + public String getType() { + return type; + } + + + @JsonProperty(JSON_PROPERTY_TYPE) + @JsonInclude(value = JsonInclude.Include.ALWAYS) + public void setType(String type) { + this.type = type; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ValidationError validationError = (ValidationError) o; + return Objects.equals(this.loc, validationError.loc) && + Objects.equals(this.msg, validationError.msg) && + Objects.equals(this.type, validationError.type); + } + + @Override + public int hashCode() { + return Objects.hash(loc, msg, type); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ValidationError {\n"); + sb.append(" loc: ").append(toIndentedString(loc)).append("\n"); + sb.append(" msg: ").append(toIndentedString(msg)).append("\n"); + sb.append(" type: ").append(toIndentedString(type)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationErrorLocInner.java b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationErrorLocInner.java new file mode 100644 index 00000000..3f3658e3 --- /dev/null +++ b/server/application-server/src/main/java/de/tum/in/www1/hephaestus/intelligenceservice/model/ValidationErrorLocInner.java @@ -0,0 +1,70 @@ +/* + * Hephaestus Intelligence Service API + * API documentation for the Hephaestus Intelligence Service. + * + * The version of the OpenAPI document: 0.0.1 + * Contact: felixtj.dietrich@tum.de + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package de.tum.in.www1.hephaestus.intelligenceservice.model; + +import java.util.Objects; +import java.util.Arrays; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import com.fasterxml.jackson.annotation.JsonTypeName; +import org.hibernate.validator.constraints.*; + +/** + * ValidationErrorLocInner + */ +@JsonPropertyOrder({ +}) +@JsonTypeName("ValidationError_loc_inner") +@jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen", comments = "Generator version: 7.7.0") +public class ValidationErrorLocInner { + public ValidationErrorLocInner() { + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + return true; + } + + @Override + public int hashCode() { + return Objects.hash(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class ValidationErrorLocInner {\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + +} + diff --git a/server/intelligence-service/app/config.py b/server/intelligence-service/app/config.py index f153b6d8..5383e680 100644 --- a/server/intelligence-service/app/config.py +++ b/server/intelligence-service/app/config.py @@ -1,7 +1,12 @@ -from pydantic_settings import BaseSettings +from dotenv import load_dotenv +from pydantic_settings import BaseSettings, SettingsConfigDict + +load_dotenv() class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env") + OPENAI_API_KEY: str = "" AZURE_OPENAI_API_KEY: str = "" @@ -11,9 +16,11 @@ class Settings(BaseSettings): @property def is_openai_available(self): return bool(self.OPENAI_API_KEY) - + @property def is_azure_openai_available(self): - return bool(self.AZURE_OPENAI_API_KEY) and bool(self.AZURE_OPENAI_ENDPOINT) and bool(self.AZURE_OPENAI_API_VERSION) + return bool(self.AZURE_OPENAI_API_KEY) and bool(self.AZURE_OPENAI_ENDPOINT) and bool( + self.AZURE_OPENAI_API_VERSION) + settings = Settings() diff --git a/server/intelligence-service/app/generate_openapi_yaml.py b/server/intelligence-service/app/generate_openapi_yaml.py new file mode 100644 index 00000000..c9c2f3ce --- /dev/null +++ b/server/intelligence-service/app/generate_openapi_yaml.py @@ -0,0 +1,30 @@ +from . import main +from fastapi.openapi.utils import get_openapi +import yaml + + +def get_openapi_specs(): + openapi_json = get_openapi( + title=main.app.title, + version=main.app.version, + description=main.app.description, + contact=main.app.contact, + routes=main.app.routes, + ) + openapi_yaml = yaml.dump(openapi_json, allow_unicode=True) + return openapi_yaml + + +def convert(): + try: + yaml_spec = get_openapi_specs() + with open("server/intelligence-service/openapi.yaml", "w") as f: + f.write(yaml_spec) + print("OpenAPI YAML specification generated successfully.") + except Exception as e: + print(f"Error generating OpenAPI specs: {e}") + exit(1) + + +if __name__ == "__main__": + convert() diff --git a/server/intelligence-service/app/main.py b/server/intelligence-service/app/main.py index 22560ae7..dd46f0f8 100644 --- a/server/intelligence-service/app/main.py +++ b/server/intelligence-service/app/main.py @@ -2,17 +2,27 @@ from pydantic import BaseModel from .model import model -app = FastAPI() + +app = FastAPI( + title="Hephaestus Intelligence Service API", + description="API documentation for the Hephaestus Intelligence Service.", + version="0.0.1", + contact={"name": "Felix T.J. Dietrich", "email": "felixtj.dietrich@tum.de"}, +) class ChatRequest(BaseModel): message: str -@app.post("/chat", response_model=dict, summary="Chat with LLM") +class ChatResponse(BaseModel): + response: str + + +@app.post("/chat", response_model=ChatResponse, summary="Get a response from an LLM to a chat message.") async def chat(request: ChatRequest): try: response = model.invoke(request.message) - return { "response": response.content } + return {"response": response.content} except Exception as e: raise HTTPException(status_code=500, detail=str(e)) diff --git a/server/intelligence-service/app/model.py b/server/intelligence-service/app/model.py index e890fed0..ecacc241 100644 --- a/server/intelligence-service/app/model.py +++ b/server/intelligence-service/app/model.py @@ -1,11 +1,18 @@ +import os from langchain.chat_models.base import BaseChatModel from langchain_openai import ChatOpenAI, AzureChatOpenAI -from .config import settings +from .config import settings model: BaseChatModel -if settings.is_openai_available: +if os.getenv("GITHUB_ACTIONS") == "true": + class MockChatModel(): + def invoke(self, message: str): + return "Mock response" + model = MockChatModel() + +elif settings.is_openai_available: model = ChatOpenAI() elif settings.is_azure_openai_available: model = AzureChatOpenAI() diff --git a/server/intelligence-service/openapi.yaml b/server/intelligence-service/openapi.yaml new file mode 100644 index 00000000..d33d3e8d --- /dev/null +++ b/server/intelligence-service/openapi.yaml @@ -0,0 +1,82 @@ +components: + schemas: + ChatRequest: + properties: + message: + title: Message + type: string + required: + - message + title: ChatRequest + type: object + ChatResponse: + properties: + response: + title: Response + type: string + required: + - response + title: ChatResponse + type: object + HTTPValidationError: + properties: + detail: + items: + $ref: '#/components/schemas/ValidationError' + title: Detail + type: array + title: HTTPValidationError + type: object + ValidationError: + properties: + loc: + items: + anyOf: + - type: string + - type: integer + title: Location + type: array + msg: + title: Message + type: string + type: + title: Error Type + type: string + required: + - loc + - msg + - type + title: ValidationError + type: object +info: + contact: + email: felixtj.dietrich@tum.de + name: Felix T.J. Dietrich + description: API documentation for the Hephaestus Intelligence Service. + title: Hephaestus Intelligence Service API + version: 0.0.1 +openapi: 3.1.0 +paths: + /chat: + post: + operationId: chat_chat_post + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ChatRequest' + required: true + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ChatResponse' + description: Successful Response + '422': + content: + application/json: + schema: + $ref: '#/components/schemas/HTTPValidationError' + description: Validation Error + summary: Get a response from an LLM to a chat message. diff --git a/server/intelligence-service/openapitools.json b/server/intelligence-service/openapitools.json new file mode 100644 index 00000000..2f4612ce --- /dev/null +++ b/server/intelligence-service/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "7.8.0" + } +}