diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
new file mode 100644
index 0000000..bd8e261
--- /dev/null
+++ b/.devcontainer/Dockerfile
@@ -0,0 +1,23 @@
+# syntax=docker/dockerfile:1
+FROM debian:bookworm-slim
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ libxkbcommon0 \
+ ca-certificates \
+ ca-certificates-java \
+ make \
+ curl \
+ git \
+ openjdk-17-jdk-headless \
+ unzip \
+ libc++1 \
+ vim \
+ && apt-get clean autoclean
+
+# Ensure UTF-8 encoding
+ENV LANG=C.UTF-8
+ENV LC_ALL=C.UTF-8
+
+WORKDIR /workspace
+
+COPY . /workspace
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 0000000..d55fc4d
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,20 @@
+// For format details, see https://aka.ms/devcontainer.json. For config options, see the
+// README at: https://github.com/devcontainers/templates/tree/main/src/debian
+{
+ "name": "Debian",
+ "build": {
+ "dockerfile": "Dockerfile"
+ }
+
+ // Features to add to the dev container. More info: https://containers.dev/features.
+ // "features": {},
+
+ // Use 'forwardPorts' to make a list of ports inside the container available locally.
+ // "forwardPorts": [],
+
+ // Configure tool-specific properties.
+ // "customizations": {},
+
+ // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
+ // "remoteUser": "root"
+}
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..022b841
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,5 @@
+#
+# https://help.github.com/articles/dealing-with-line-endings/
+#
+# These are explicitly windows files and should use crlf
+*.bat text eol=crlf
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..02410e7
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,85 @@
+name: CI
+on:
+ push:
+ branches-ignore:
+ - 'generated'
+ - 'codegen/**'
+ - 'integrated/**'
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
+ pull_request:
+ branches-ignore:
+ - 'stl-preview-head/**'
+ - 'stl-preview-base/**'
+
+jobs:
+ lint:
+ timeout-minutes: 15
+ name: lint
+ runs-on: ${{ github.repository == 'stainless-sdks/imagekit-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Run lints
+ run: ./scripts/lint
+
+ build:
+ timeout-minutes: 15
+ name: build
+ runs-on: ${{ github.repository == 'stainless-sdks/imagekit-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/actions/setup-gradle@v4
+
+ - name: Build SDK
+ run: ./scripts/build
+
+ test:
+ timeout-minutes: 15
+ name: test
+ runs-on: ${{ github.repository == 'stainless-sdks/imagekit-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }}
+ if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Run tests
+ run: ./scripts/test
diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml
new file mode 100644
index 0000000..9207f7f
--- /dev/null
+++ b/.github/workflows/publish-sonatype.yml
@@ -0,0 +1,41 @@
+# This workflow is triggered when a GitHub release is created.
+# It can also be run manually to re-publish to Sonatype in case it failed for some reason.
+# You can run this workflow by navigating to https://www.github.com/imagekit-developer/imagekit-java/actions/workflows/publish-sonatype.yml
+name: Publish Sonatype
+on:
+ workflow_dispatch:
+
+ release:
+ types: [published]
+
+jobs:
+ publish:
+ name: publish
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Java
+ uses: actions/setup-java@v4
+ with:
+ distribution: temurin
+ java-version: |
+ 8
+ 21
+ cache: gradle
+
+ - name: Set up Gradle
+ uses: gradle/gradle-build-action@v2
+
+ - name: Publish to Sonatype
+ run: |-
+ export -- GPG_SIGNING_KEY_ID
+ printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
+ GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
+ ./gradlew publish --no-configuration-cache
+ env:
+ SONATYPE_USERNAME: ${{ secrets.IMAGE_KIT_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.IMAGE_KIT_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+ GPG_SIGNING_KEY: ${{ secrets.IMAGE_KIT_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+ GPG_SIGNING_PASSWORD: ${{ secrets.IMAGE_KIT_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
\ No newline at end of file
diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml
new file mode 100644
index 0000000..4b394d2
--- /dev/null
+++ b/.github/workflows/release-doctor.yml
@@ -0,0 +1,24 @@
+name: Release Doctor
+on:
+ pull_request:
+ branches:
+ - master
+ workflow_dispatch:
+
+jobs:
+ release_doctor:
+ name: release doctor
+ runs-on: ubuntu-latest
+ if: github.repository == 'imagekit-developer/imagekit-java' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next')
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Check release environment
+ run: |
+ bash ./bin/check-release-environment
+ env:
+ SONATYPE_USERNAME: ${{ secrets.IMAGE_KIT_SONATYPE_USERNAME || secrets.SONATYPE_USERNAME }}
+ SONATYPE_PASSWORD: ${{ secrets.IMAGE_KIT_SONATYPE_PASSWORD || secrets.SONATYPE_PASSWORD }}
+ GPG_SIGNING_KEY: ${{ secrets.IMAGE_KIT_SONATYPE_GPG_SIGNING_KEY || secrets.GPG_SIGNING_KEY }}
+ GPG_SIGNING_PASSWORD: ${{ secrets.IMAGE_KIT_SONATYPE_GPG_SIGNING_PASSWORD || secrets.GPG_SIGNING_PASSWORD }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index b1a3bc8..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-# This workflow will build a Java project with Gradle
-# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-gradle
-
-name: Java CI
-
-on: [push, pull_request]
-
-jobs:
- build:
-
- runs-on: ubuntu-latest
-
- steps:
- - uses: actions/checkout@v2
- - name: Set up JDK 1.8
- uses: actions/setup-java@v1
- with:
- java-version: 1.8
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
- - name: Running Test Cases
- run: ./gradlew imagekit-sdk:test
diff --git a/.gitignore b/.gitignore
index bc21173..b1346e6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,7 @@
-.idea
-out/production
+.prism.log
.gradle
-build
-config.properties
-.DS_Store
-.project
-.settings
-bin
-.classpath
+.idea
+.kotlin
+build/
+codegen.log
+kls_database.db
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
new file mode 100644
index 0000000..3d2ac0b
--- /dev/null
+++ b/.release-please-manifest.json
@@ -0,0 +1,3 @@
+{
+ ".": "0.1.0"
+}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
new file mode 100644
index 0000000..bbfdcb4
--- /dev/null
+++ b/.stats.yml
@@ -0,0 +1,4 @@
+configured_endpoints: 42
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/imagekit-inc%2Fimagekit-bc7c0d27962b30c19c778656988e154b54696819389289f34420a5e5fdfbd3b8.yml
+openapi_spec_hash: 1bfde02a63416c036e9545927f727459
+config_hash: a652d68098d82eaf611a49507fb4b831
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..c031daa
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,47 @@
+# Changelog
+
+## 0.1.0 (2025-09-04)
+
+Full Changelog: [v0.0.1...v0.1.0](https://github.com/imagekit-developer/imagekit-java/compare/v0.0.1...v0.1.0)
+
+### Features
+
+* **api:** add BaseWebhookEvent ([4ab06b0](https://github.com/imagekit-developer/imagekit-java/commit/4ab06b01316b114e526159de348b9d52fdd57567))
+* **api:** manual updates ([2ef93ad](https://github.com/imagekit-developer/imagekit-java/commit/2ef93ad3db97af5fef99687a4438c76e2220b384))
+* **api:** manual updates ([c6e61f8](https://github.com/imagekit-developer/imagekit-java/commit/c6e61f8ca6b3a9485ab15d0221842ef76b6cae32))
+* **api:** manual updates ([1c76234](https://github.com/imagekit-developer/imagekit-java/commit/1c762344ee41aa7099febdca72f368943ee8428b))
+* **api:** manual updates ([cd2b64e](https://github.com/imagekit-developer/imagekit-java/commit/cd2b64ef8d6324264a71321bb11a7849260d98fc))
+* **api:** manual updates ([1341b62](https://github.com/imagekit-developer/imagekit-java/commit/1341b6282b4a32013cb60f11ee0ec9aaef8acac4))
+* **api:** manual updates ([83ce070](https://github.com/imagekit-developer/imagekit-java/commit/83ce0707ff45a9daa97e12daa9ad050dcbab53f9))
+* **api:** manual updates ([d57885b](https://github.com/imagekit-developer/imagekit-java/commit/d57885b731de1837a7edcd3f6eab303db855475a))
+* **api:** manual updates ([74027ef](https://github.com/imagekit-developer/imagekit-java/commit/74027efe9095a5e650addddf064d6e3773413894))
+* **api:** manual updates ([76c5b5e](https://github.com/imagekit-developer/imagekit-java/commit/76c5b5e1fbc0ac3415d5e35febce642dd47cd0ab))
+* **api:** manual updates ([b462410](https://github.com/imagekit-developer/imagekit-java/commit/b46241040326016adf4ad220178ccf3221323f90))
+* **api:** manual updates ([cbbd424](https://github.com/imagekit-developer/imagekit-java/commit/cbbd424da5ebd388dd6e3d8af7e38b7633ed0604))
+
+
+### Bug Fixes
+
+* add one more method in multipartbuilder to build from json ([9dc2f89](https://github.com/imagekit-developer/imagekit-java/commit/9dc2f897ec7b9a1998d283359472b882cfb9ee68))
+* **ci:** use java-version 21 for publish step ([181fbe5](https://github.com/imagekit-developer/imagekit-java/commit/181fbe5360fbc5820cb9df684d242896ec146049))
+
+
+### Chores
+
+* added reauired constants ([85ffc4d](https://github.com/imagekit-developer/imagekit-java/commit/85ffc4d8ee3a39546f45c4abe5b26fc3a9628cfe))
+* ImageKit class added with all functionalities ([6edfbd1](https://github.com/imagekit-developer/imagekit-java/commit/6edfbd19d82826e76ed913512ef518d31859b440))
+* ImageKit Configuration class added ([3af7846](https://github.com/imagekit-developer/imagekit-java/commit/3af7846d0f91f43e63ac71c2d0f7f6b73ca3250d))
+* Initial setup ([c70634b](https://github.com/imagekit-developer/imagekit-java/commit/c70634b76b4a4f1299d73b2a6a20df128bf3c088))
+* response models, MultipartBuilder and RestClient created for file operations ([ed443a7](https://github.com/imagekit-developer/imagekit-java/commit/ed443a793f765f296ed51119656b017ab1dccfe3))
+* sample sdk running code added ([46e0c29](https://github.com/imagekit-developer/imagekit-java/commit/46e0c29bc2d3e6bfbf001870c11de08d9260e896))
+* sync repo ([05af7d5](https://github.com/imagekit-developer/imagekit-java/commit/05af7d57aab0174c720f1dd71e87d39f1197bfa1))
+* update SDK settings ([00f7055](https://github.com/imagekit-developer/imagekit-java/commit/00f705596f585c9020a4620a9675e733f28b8b1b))
+* url generation functionality and getAuthentication functionality implemented ([d8c2b5d](https://github.com/imagekit-developer/imagekit-java/commit/d8c2b5d7fe098da89e0ca2804a03032a8155ed17))
+* Utils class added ([fdfe731](https://github.com/imagekit-developer/imagekit-java/commit/fdfe73124974ade54e03705421149f89469be86d))
+
+
+### Build System
+
+* Test CI added ([16153cc](https://github.com/imagekit-developer/imagekit-java/commit/16153ccc9b7a49f7dc732974e0b4b384d8273ea2))
+* updating CI ([a14b7cc](https://github.com/imagekit-developer/imagekit-java/commit/a14b7cc70204cd8af648e3c3ac7664f3916850b1))
+* version and doc changed for expecting release of 1.0.0 ([0abfb43](https://github.com/imagekit-developer/imagekit-java/commit/0abfb433ef126b5e21acf8773733b1003153a7e4))
diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md
deleted file mode 100644
index f220036..0000000
--- a/DEVELOPMENT.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# Development Guide
-
-
-**1. To modify version open `imagekit-sdk/build.gradle` and change value of `version`.**
-
-**2. Run test cases**
-```shell
-./gradlew imagekit-sdk:test
-```
-
-**3. Clean cache of SDK**
-```shell
-./gradlew imagekit-sdk:clean
-```
-
-**4. Build SDK jar**
-```shell script
-./gradlew imagekit-sdk:build
-```
-Then you will find `jar` inside `imagekit-sdk/build/libs/` with specific version name. Then you can share `jar` or upload `jar` to any java dependency repository.
-
diff --git a/LICENSE b/LICENSE
index 2d96bbb..e7a4d16 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,201 @@
-MIT License
-
-Copyright (c) 2020 Imagekit
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright 2025 Image Kit
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/README.md b/README.md
index c608840..7880086 100644
--- a/README.md
+++ b/README.md
@@ -1,1209 +1,710 @@
-# ImageKit Java SDK
-
-[](https://github.com/imagekit-developer/imagekit-java)
-[](https://jitpack.io/#com.github.imagekit-developer/imagekit-java)
-[](https://opensource.org/licenses/MIT)
-[](https://twitter.com/ImagekitIo)
-
-Java SDK for [ImageKit.io](https://imagekit.io/) that implements the new APIs and interface for performing different file operations.
-
-ImageKit is complete media storage, optimization, and transformation solution that comes with an [image and video CDN](https://imagekit.io). It can be integrated with your existing infrastructure - storage like AWS S3, web servers, your CDN, and custom domain names, allowing you to deliver optimized images in minutes with minimal code changes.
-
-Table of contents -
- * [Installation](#installation)
- * [Initialization](#initialization)
- * [Usage](#usage)
- * [Versioning](#versioning)
- * [URL generation](#url-generation)
- * [File upload](#file-upload)
- * [File management](#file-management)
- * [Utility functions](#utility-functions)
- * [Handling errors](#handling-errors)
- * [Support](#support)
- * [Links](#links)
-
-## Installation
+# Image Kit Java API Library
-### Requirements
+
-- Java 1.8 or later
+[](https://central.sonatype.com/artifact/com.imagekit.api/image-kit-java/0.1.0)
+[](https://javadoc.io/doc/com.imagekit.api/image-kit-java/0.1.0)
-### Gradle users
-Step 1. Add the JitPack repository to your build file
-```
-allprojects {
- repositories {
- ...
- maven { url 'https://jitpack.io' }
- }
-}
-```
-Step 2. Add the dependency on the project's `build.gradle`:
-```
-dependencies {
- implementation 'com.github.imagekit-developer:imagekit-java:2.0.0'
-}
-```
-### Maven users
-Step 1. Add the JitPack repository to your build file
-```
-
-
- jitpack.io
- https://jitpack.io
-
-
-```
-Step 2. Add the dependency in the POM file:
-```
-
- com.github.imagekit-developer
- imagekit-java
- 2.0.0
-
-```
+
-## Initialization
+The Image Kit Java SDK provides convenient access to the [Image Kit REST API](https://imagekit.io/docs) from applications written in Java.
-**Step 1**. Create a `config.properties` file inside `src/main/resources` of your project. And put essential values of keys [UrlEndpoint, PrivateKey, PublicKey], no need to use quote(`'` or `"`) in values.
+It is generated with [Stainless](https://www.stainless.com/).
- You can get the value of [URL-endpoint](https://imagekit.io/dashboard#url-endpoints) from your ImageKit dashboard. API keys can be obtained from the [developer](https://imagekit.io/dashboard/developer/api-keys) section in your ImageKit dashboard.
+
-```editorconfig
-# Put essential values of keys [UrlEndpoint, PrivateKey, PublicKey]
-UrlEndpoint=your_public_api_key
-PrivateKey=your_private_api_key
-PublicKey=https://ik.imagekit.io/imagekit_id/
-```
+The REST API documentation can be found on [imagekit.io](https://imagekit.io/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.imagekit.api/image-kit-java/0.1.0).
-**Step 2**. Then you need to initialize ImageKit with that configuration.
-
- ```java
-import io.imagekit.sdk.ImageKit;
-import io.imagekit.sdk.config.Configuration;
-import io.imagekit.sdk.utils.Utils;
-class App {
- public static void main(String[] args){
- ImageKit imageKit=ImageKit.getInstance();
- Configuration config=Utils.getSystemConfig(App.class);
- imageKit.setConfig(config);
- }
-}
-```
+
-or
-
- ```java
-import io.imagekit.sdk.ImageKit;
-import io.imagekit.sdk.config.Configuration;
-import io.imagekit.sdk.utils.Utils;
-class App {
- public static void main(String[] args) {
- ImageKit imageKit = ImageKit.getInstance();
- Configuration config = new Configuration("your_public_key", "your_private_key", "your_url_endpoint");
- imageKit.setConfig(config);
- }
-}
-```
+## Installation
-## Usage
-You can use this Java SDK for 3 different kinds of methods:
+
-* URL generation
-* file upload
-* file management
+### Gradle
-The usage of the SDK has been explained below.
+```kotlin
+implementation("com.imagekit.api:image-kit-java:0.1.0")
+```
-## Change log
-This document presents a list of changes that break the existing functionality of previous versions. We try our best to minimize these disruptions, but sometimes they are unavoidable and will be in major versions.
+### Maven
-### Breaking History:
+```xml
+
+ com.imagekit.api
+ image-kit-java
+ 0.1.0
+
+```
-Changes from 1.0.3 -> 2.0.0 are listed below
+
-1. Result `raw` object and `getMap()` properties:
+## Requirements
-**What changed**
-- `raw` and `getMap()` has been deprecated.
+This library requires Java 8 or later.
-**Who is affected?**
-- This affects any development that uses the `raw` or `getMap()` from the response object of APIs and Result object.
+## Usage
-**How should I update my code?**
-- If you still need to use `raw` and `getMap()`, do this `result.getResponseMetaData().getRaw()`.
-
-2. Result object `message` and `isSuccessful` boolean properties:
+```java
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.io.ByteArrayInputStream;
-**What changed**
-- `message` and `isSuccessful` have been replaced with custom exceptions according to response code.
+// Configures using the `imagekit.imagekitPrivateApiKey`, `imagekit.optionalImagekitIgnoresThis`, `imagekit.imagekitWebhookSecret` and `imagekit.baseUrl` system properties
+// Or configures using the `IMAGEKIT_PRIVATE_API_KEY`, `OPTIONAL_IMAGEKIT_IGNORES_THIS`, `IMAGEKIT_WEBHOOK_SECRET` and `IMAGE_KIT_BASE_URL` environment variables
+ImageKitClient client = ImageKitOkHttpClient.fromEnv();
-**Who is affected?**
-- This affects any development that uses the `message` or `isSuccessful` from response object of APIs that is Result object.
+FileUploadParams params = FileUploadParams.builder()
+ .file(ByteArrayInputStream("https://www.example.com/public-url.jpg".getBytes()))
+ .fileName("file-name.jpg")
+ .build();
+FileUploadResponse response = client.files().upload(params);
+```
-**How should I update my code?**
-- If you still need to use `message` it will be there in the custom exceptions that could be raised when calling the various API methods. `isSuccessful` can be understood to be `true` if the API method doesn't throw any exception.
+## Client configuration
+Configure the client using system properties or environment variables:
-## URL generation
+```java
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
-**1. Using image path and URL-endpoint**
+// Configures using the `imagekit.imagekitPrivateApiKey`, `imagekit.optionalImagekitIgnoresThis`, `imagekit.imagekitWebhookSecret` and `imagekit.baseUrl` system properties
+// Or configures using the `IMAGEKIT_PRIVATE_API_KEY`, `OPTIONAL_IMAGEKIT_IGNORES_THIS`, `IMAGEKIT_WEBHOOK_SECRET` and `IMAGE_KIT_BASE_URL` environment variables
+ImageKitClient client = ImageKitOkHttpClient.fromEnv();
+```
-This method allows you to create an URL to access a file using the relative file path and the ImageKit URL endpoint (`urlEndpoint`). The file can be an image, video, or any other static file supported by ImageKit.
+Or manually:
```java
-Map queryParam=new HashMap<>();
-queryParam.put("v","123");
-
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","600");
-scale.put("width","400");
-scale.put("raw", "ar-4-3,q-40");
-transformation.add(scale);
-
-Map options=new HashMap();
-options.put("urlEndpoint","https://ik.imagekit.io/your_imagekit_id/");
-options.put("path","/default-image.jpg");
-options.put("transformation", transformation);
-
-String url = ImageKit.getInstance().getUrl(options);
-```
-The result in a URL like
-```
-https://ik.imagekit.io/your_imagekit_id/tr:w-400,h-600/default-image.jpg?v=123
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ .privateApiKey("My Private API Key")
+ .password("My Password")
+ .build();
```
-**2. Using full image URL**
-This method allows you to add transformation parameters to an absolute URL. For example, if you have configured a custom CNAME and have absolute asset URLs in your database or CMS, you will often need this.
+Or using a combination of the two approaches:
```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","600");
-scale.put("width","400");
-scale.put("raw", "ar-4-3,q-40");
-transformation.add(scale);
-
-Map options=new HashMap();
-options.put("src","https://ik.imagekit.io/your_imagekit_id/default-image.jpg");
-options.put("transformation", transformation);
-
-String url = ImageKit.getInstance().getUrl(options);
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ // Configures using the `imagekit.imagekitPrivateApiKey`, `imagekit.optionalImagekitIgnoresThis`, `imagekit.imagekitWebhookSecret` and `imagekit.baseUrl` system properties
+ // Or configures using the `IMAGEKIT_PRIVATE_API_KEY`, `OPTIONAL_IMAGEKIT_IGNORES_THIS`, `IMAGEKIT_WEBHOOK_SECRET` and `IMAGE_KIT_BASE_URL` environment variables
+ .fromEnv()
+ .privateApiKey("My Private API Key")
+ .build();
```
-The results in a URL like
+See this table for the available options:
-```
-https://ik.imagekit.io/your_imagekit_id/default-image.jpg?tr=w-400,h-600
-```
+| Setter | System property | Environment variable | Required | Default value |
+| --------------- | -------------------------------------- | -------------------------------- | -------- | --------------------------- |
+| `privateApiKey` | `imagekit.imagekitPrivateApiKey` | `IMAGEKIT_PRIVATE_API_KEY` | true | - |
+| `password` | `imagekit.optionalImagekitIgnoresThis` | `OPTIONAL_IMAGEKIT_IGNORES_THIS` | false | `"do_not_set"` |
+| `webhookSecret` | `imagekit.imagekitWebhookSecret` | `IMAGEKIT_WEBHOOK_SECRET` | false | - |
+| `baseUrl` | `imagekit.baseUrl` | `IMAGE_KIT_BASE_URL` | true | `"https://api.imagekit.io"` |
-The ```.getUrl()``` method accepts the following parameters
+System properties take precedence over environment variables.
-| Option | Description |
-| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| urlEndpoint | Optional. `(Type: String)` The base URL to be appended before the path of the image. If not specified, the URL Endpoint specified during SDK initialization is used. For example, https://ik.imagekit.io/your_imagekit_id/ |
-| path | Conditional. `(Type: String)` This is the path at which the image exists. For example, `/path/to/image.jpg`. Either the `path` or `src` parameter needs to be specified for URL generation. |
-| src | Conditional. `(Type: String)` This is the complete URL of an image already mapped to ImageKit. For example, `https://ik.imagekit.io/your_imagekit_id/endpoint/path/to/image.jpg`. Either the `path` or `src` parameter needs to be specified for URL generation. |
-| transformation | Optional. `(Type: List>)` An array of objects specifying the transformation to be applied in the URL. The transformation name and the value should be specified as a key-value pair in the object. Different steps of a [chained transformation](https://docs.imagekit.io/features/image-transformations/chained-transformations) can be specified as different objects of the array. The complete list of supported transformations in the SDK and some examples of using them are given later. If you use a transformation name that is not specified in the SDK, it gets applied as it is in the URL. |
-| transformationPosition | Optional. `(Type: String)` Default value is `path` that places the transformation string as a path parameter in the URL. It can also be specified as `query`, which adds the transformation string as the query parameter `tr` in the URL. If you use the `src` parameter to create the URL, then the transformation string is always added as a query parameter. |
-| queryParameters | Optional. `(Type: Map)` These are the other query parameters that you want to add to the final URL. These can be any query parameters and not necessarily related to ImageKit. Especially useful if you want to add some versioning parameters to your URLs. |
-| signed | Optional. `(Type: Boolean)` Default is `false`. If set to `true`, the SDK generates a signed image URL adding the image signature to the image URL. This can only be used if you create the URL with the `url_endpoint` and `path` parameters and not with the `src` parameter. |
-| expireSeconds | Optional. `(Type: Integer)` Meant to be used along with the `signed` parameter to specify the time in seconds from now when the URL should expire. If specified, the URL contains the expiry timestamp in the URL, and the image signature is modified accordingly. |
+> [!TIP]
+> Don't create more than one client in the same application. Each client has a connection pool and
+> thread pools, which are more efficient to share between requests.
+### Modifying configuration
-## Examples of generating URLs
-**1. Chained Transformations as a query parameter**
+To temporarily use a modified client configuration, while reusing the same connection and thread pools, call `withOptions()` on any client or service:
```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","300");
-scale.put("width","400");
-transformation.add(scale);
-Map rotate=new HashMap<>();
-rotate.put("rotation","90");
-transformation.add(rotate);
-
-Map options=new HashMap();
-options.put("path","/default-image.jpg");
-options.put("transformationPosition","query");
-options.put("transformation", transformation);
-
-String url = ImageKit.getInstance().getUrl(options);
-```
+import com.imagekit.api.client.ImageKitClient;
-Sample Result URL -
-```
-https://ik.imagekit.io/your_imagekit_id/default-image.jpg?tr=h-300&w-400:rt-90
+ImageKitClient clientWithOptions = client.withOptions(optionsBuilder -> {
+ optionsBuilder.baseUrl("https://example.com");
+ optionsBuilder.maxRetries(42);
+});
```
-**2. Sharpening and contrast transform and a progressive JPG image**
+The `withOptions()` method does not affect the original client or service.
-There are some transforms like [Sharpening](https://docs.imagekit.io/features/image-transformations/image-enhancement-and-color-manipulation)
-that can be added to the URL with or without any other value. To use such transforms without specifying a value, specify
-the value as "-" in the transformation object. Otherwise, specify the value that you want to be
-added to this transformation.
+## Requests and responses
-```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("format","jpg");
-scale.put("progressive","true");
-scale.put("effect_sharpen","-");
-scale.put("effect_contrast","1");
-transformation.add(scale);
-
-Map options=new HashMap();
-options.put("path","/default-image.jpg");
-options.put("transformation", transformation);
-String url = ImageKit.getInstance().getUrl(options);
-```
-
-Note that because the `src` parameter was used, the transformation string gets added as a query parameter.
-
-```
-https://ik.imagekit.io/your_imagekit_id/default-image.jpg?tr=f-jpg&pr-true&e-sharpen&e-contrast-1
-```
+To send a request to the Image Kit API, build an instance of some `Params` class and pass it to the corresponding client method. When the response is received, it will be deserialized into an instance of a Java class.
-**3. Signed URL that expires in 300 seconds with the default URL endpoint and other query parameters**
+For example, `client.files().upload(...)` should be called with an instance of `FileUploadParams`, and it will return an instance of `FileUploadResponse`.
-```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","600");
-scale.put("width","400");
-
-transformation.add(format);
-
-Map options=new HashMap();
-options.put("path","/default-image.jpg");
-options.put("signed",true);
-options.put("expireSeconds",300);
-String url = ImageKit.getInstance().getUrl(options);
-```
-**Sample Result URL**
-```
-https://ik.imagekit.io/your_imagekit_id/tr:h-600,w-400/default-image.jpg?ik-t=1567358667&ik-s=f2c7cdacbe7707b71a83d49cf1c6110e3d701054
-```
+## Immutability
-**4. Adding overlays**
+Each class in the SDK has an associated [builder](https://blogs.oracle.com/javamagazine/post/exploring-joshua-blochs-builder-design-pattern-in-java) or factory method for constructing it.
-ImageKit.io enables you to apply overlays to [images](https://docs.imagekit.io/features/image-transformations/overlay-using-layers) and [videos](https://docs.imagekit.io/features/video-transformation/overlay) using the raw parameter with the concept of [layers](https://docs.imagekit.io/features/image-transformations/overlay-using-layers#layers). The raw parameter facilitates incorporating transformations directly in the URL. A layer is a distinct type of transformation that allows you to define an asset to serve as an overlay, along with its positioning and additional transformations.
+Each class is [immutable](https://docs.oracle.com/javase/tutorial/essential/concurrency/immutable.html) once constructed. If the class has an associated builder, then it has a `toBuilder()` method, which can be used to convert it back to a builder for making a modified copy.
-**Text as overlays**
+Because each class is immutable, builder modification will _never_ affect already built class instances.
-You can add any text string over a base video or image using a text layer (l-text).
+## Asynchronous execution
-For example:
+The default client is synchronous. To switch to asynchronous execution, call the `async()` method:
```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","300");
-scale.put("width","400");
-scale.put("raw", "l-text,i-Imagekit,fs-50,l-end");
-transformation.add(scale);
-
-Map options=new HashMap();
-options.put("src","https://ik.imagekit.io/your_imagekit_id/default-image.jpg");
-options.put("transformation", transformation);
-
-String url = ImageKit.getInstance().getUrl(options);
-```
-**Sample Result URL**
-```
-https://ik.imagekit.io/your_imagekit_id/default-image.jpg?tr=h-300,w-400,l-text,i-Imagekit,fs-50,l-end
-```
-
-**Image as overlays**
-
-You can add an image over a base video or image using an image layer (l-image).
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.io.ByteArrayInputStream;
+import java.util.concurrent.CompletableFuture;
-For example:
+// Configures using the `imagekit.imagekitPrivateApiKey`, `imagekit.optionalImagekitIgnoresThis`, `imagekit.imagekitWebhookSecret` and `imagekit.baseUrl` system properties
+// Or configures using the `IMAGEKIT_PRIVATE_API_KEY`, `OPTIONAL_IMAGEKIT_IGNORES_THIS`, `IMAGEKIT_WEBHOOK_SECRET` and `IMAGE_KIT_BASE_URL` environment variables
+ImageKitClient client = ImageKitOkHttpClient.fromEnv();
-```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","300");
-scale.put("width","400");
-scale.put("raw", "l-image,i-default-image.jpg,w-100,b-10_CDDC39,l-end");
-transformation.add(scale);
-
-Map options=new HashMap();
-options.put("src","https://ik.imagekit.io/your_imagekit_id/default-image.jpg");
-options.put("transformation", transformation);
-
-String url = ImageKit.getInstance().getUrl(options);
-```
-**Sample Result URL**
-```
-https://ik.imagekit.io/your_imagekit_id/default-image.jpg?tr=h-300,w-400,l-image,i-default-image.jpg,w-100,b-10_CDDC39,l-end
+FileUploadParams params = FileUploadParams.builder()
+ .file(ByteArrayInputStream("https://www.example.com/public-url.jpg".getBytes()))
+ .fileName("file-name.jpg")
+ .build();
+CompletableFuture response = client.async().files().upload(params);
```
-**Solid color blocks as overlays**
+Or create an asynchronous client from the beginning:
-You can add solid color blocks over a base video or image using an image layer (l-image).
+```java
+import com.imagekit.api.client.ImageKitClientAsync;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClientAsync;
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.io.ByteArrayInputStream;
+import java.util.concurrent.CompletableFuture;
-For example:
+// Configures using the `imagekit.imagekitPrivateApiKey`, `imagekit.optionalImagekitIgnoresThis`, `imagekit.imagekitWebhookSecret` and `imagekit.baseUrl` system properties
+// Or configures using the `IMAGEKIT_PRIVATE_API_KEY`, `OPTIONAL_IMAGEKIT_IGNORES_THIS`, `IMAGEKIT_WEBHOOK_SECRET` and `IMAGE_KIT_BASE_URL` environment variables
+ImageKitClientAsync client = ImageKitOkHttpClientAsync.fromEnv();
-```java
-List> transformation=new ArrayList>();
-Map scale=new HashMap<>();
-scale.put("height","300");
-scale.put("width","400");
-scale.put("raw", "l-image,i-ik_canvas,bg-FF0000,w-300,h-100,l-end");
-transformation.add(scale);
-
-Map options=new HashMap();
-options.put("src","https://ik.imagekit.io/your_imagekit_id/img/sample-video.mp4");
-options.put("transformation", transformation);
-
-String url = ImageKit.getInstance().getUrl(options);
-```
-**Sample Result URL**
-```
-https://ik.imagekit.io/your_imagekit_id/img/sample-video.mp4?tr=h-300,w-400,l-image,i-ik_canvas,bg-FF0000,w-300,h-100,l-end
+FileUploadParams params = FileUploadParams.builder()
+ .file(ByteArrayInputStream("https://www.example.com/public-url.jpg".getBytes()))
+ .fileName("file-name.jpg")
+ .build();
+CompletableFuture response = client.files().upload(params);
```
-**List of transformations**
-
-See the complete list of [image](https://docs.imagekit.io/features/image-transformations) and [video](https://docs.imagekit.io/features/video-transformation) transformations supported in ImageKit. The SDK gives a name to each transformation parameter e.g. `height` for `h` and `width` for `w` parameter. It makes your code more readable. If the property does not match any of the following supported options, it is added as it is.
-
-If you want to generate transformations in your application and add them to the URL as it is, use the `raw` parameter.
-
-| Supported Transformation Name | Translates to parameter |
-|-------------------------------|-------------------------|
-| height | h |
-| width | w |
-| aspectRatio | ar |
-| quality | q |
-| crop | c |
-| cropMode | cm |
-| x | x |
-| y | y |
-| focus | fo |
-| format | f |
-| radius | r |
-| background | bg |
-| border | b |
-| rotation | rt |
-| blur | bl |
-| named | n |
-| progressive | pr |
-| lossless | lo |
-| trim | t |
-| metadata | md |
-| colorProfile | cp |
-| defaultImage | di |
-| dpr | dpr |
-| effectSharpen | e-sharpen |
-| effectUSM | e-usm |
-| effectContrast | e-contrast |
-| effectGray | e-grayscale |
-| original | orig |
-| raw | `replaced by the parameter value` |
-
-
-## File Upload
-
-The SDK provides a simple interface using the `.upload()` method to upload files to the ImageKit Media library. It
-accepts an object of the `FileCreateRequest` class that contains all the parameters supported by the [ImageKit Upload API](https://docs.imagekit.io/api-reference/upload-file-api/server-side-file-upload).
-
-The constructor `FileCreateRequest` class requires `file` as (URL/Base64/Byte Array) and `file_name`. The method returns object of `Result` in case of successful, or it will throw custom exception in case of failure.
-
-Sample usage
+The asynchronous client supports the same options as the synchronous one, except most methods return `CompletableFuture`s.
-```java
-String filePath = "your-local-file-path";
-String base64 = Utils.fileToBase64(new File(filePath));
-FileCreateRequest fileCreateRequest = new FileCreateRequest(base64, "file_name.jpg");
-String customCoordinates = "10,10,20,20";
-fileCreateRequest.setCustomCoordinates(customCoordinates); // optional
-List tags = new ArrayList<>();
-tags.add("Sample-tag");
-tags.add("T-shirt");
-fileCreateRequest.setTags(tags); // optional
-fileCreateRequest.setFileName("override_file_name.jpg"); // optional
-fileCreateRequest.setFolder("sample-folder/nested-folder"); // optional
-fileCreateRequest.setPrivateFile(false); // optional
-fileCreateRequest.setUseUniqueFileName(true); // optional
-List responseFields=new ArrayList<>();
-responseFields.add("tags");
-responseFields.add("customCoordinates");
-fileCreateRequest.setResponseFields(responseFields); // optional
-JsonObject innerObject1 = new JsonObject();
-innerObject1.addProperty("name", "remove-bg");
-innerObject1.add("options", optionsInnerObject);
-JsonObject innerObject2 = new JsonObject();
-innerObject2.addProperty("name", "google-auto-tagging");
-innerObject2.addProperty("minConfidence", 10);
-innerObject2.addProperty("maxTags", 5);
-JsonArray jsonArray = new JsonArray();
-jsonArray.add(innerObject1);
-jsonArray.add(innerObject2);
-fileCreateRequest.setExtensions(jsonArray); // optional
-fileCreateRequest.setWebhookUrl("Your webhook url"); // optional
-fileCreateRequest.setOverwriteFile(true); // optional
-fileCreateRequest.setOverwriteAITags(true); // optional
-fileCreateRequest.setOverwriteTags(true); // optional
-fileCreateRequest.setOverwriteCustomMetadata(true); // optional
-JsonObject jsonObjectCustomMetadata = new JsonObject();
-jsonObjectCustomMetadata.addProperty("test1", 10);
-fileCreateRequest.setCustomMetadata(jsonObjectCustomMetadata); // optional
-Result result = ImageKit.getInstance().upload(fileCreateRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+## File uploads
-If the upload is successful, result will be there as an object of `Result` class that contains the same all the parameters received from ImageKit's servers.
+The SDK defines methods that accept files.
-If the upload fails, custom exception is thrown and `getMessage()` can be called to get the error message received from ImageKit's servers.
+To upload a file, pass a [`Path`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html):
+```java
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.nio.file.Paths;
-## File Management
+FileUploadParams params = FileUploadParams.builder()
+ .fileName("fileName")
+ .file(Paths.get("/path/to/file"))
+ .build();
+FileUploadResponse response = client.files().upload(params);
+```
-The SDK provides a simple interface for all the [media APIs mentioned here](https://docs.imagekit.io/api-reference/media-api) to manage your files. This also returns `error` and `result`, the error will be `None` if API succeeds.
+Or an arbitrary [`InputStream`](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html):
-**1. List & Search Files**
+```java
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.net.URL;
-Accepts an object of class `GetFileListRequest` specifying the parameters to be used to list and search files. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/list-and-search-files) can be passed via their setter functions to get the results.
+FileUploadParams params = FileUploadParams.builder()
+ .fileName("fileName")
+ .file(new URL("https://example.com//path/to/file").openStream())
+ .build();
+FileUploadResponse response = client.files().upload(params);
+```
-#### Applying Filters
-Filter out the files by specifying the parameters.
+Or a `byte[]` array:
```java
-String[] tags = new String[3];
-tags[0] = "Software";
-tags[1] = "Developer";
-tags[2] = "Engineer";
-GetFileListRequest getFileListRequest = new GetFileListRequest();
-getFileListRequest.setType("file");
-getFileListRequest.setSort("ASC_CREATED");
-getFileListRequest.setPath("/");
-getFileListRequest.setFileType("all");
-getFileListRequest.setLimit("4");
-getFileListRequest.setSkip("1");
-getFileListRequest.setTags(tags);
-ResultList resultList = ImageKit.getInstance().getFileList(getFileListRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultList);
-System.out.println("Raw Response:");
-System.out.println(resultList.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultList.getResponseMetaData().getMap());
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+
+FileUploadParams params = FileUploadParams.builder()
+ .fileName("fileName")
+ .file("content".getBytes())
+ .build();
+FileUploadResponse response = client.files().upload(params);
```
-#### Advance Search
-In addition, you can fine-tune your query by specifying various filters by generating a query string in a Lucene-like syntax and setting this generated string to the `GetFileListRequest` object using `setSearchQuery` function.
+Note that when passing a non-`Path` its filename is unknown so it will not be included in the request. To manually set a filename, pass a [`MultipartField`](image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt):
```java
-GetFileListRequest getFileListRequest = new GetFileListRequest();
-getFileListRequest.setSearchQuery("createdAt >= '2d' OR size < '2mb' OR format='png'");
-ResultList resultList = ImageKit.getInstance().getFileList(getFileListRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultList);
-System.out.println("Raw Response:");
-System.out.println(resultList.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultList.getResponseMetaData().getMap());
+import com.imagekit.api.core.MultipartField;
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.io.InputStream;
+import java.net.URL;
+
+FileUploadParams params = FileUploadParams.builder()
+ .fileName("fileName")
+ .file(MultipartField.builder()
+ .value(new URL("https://example.com//path/to/file").openStream())
+ .filename("/path/to/file")
+ .build())
+ .build();
+FileUploadResponse response = client.files().upload(params);
```
-Detailed documentation can be found here for [advance search queries](https://docs.imagekit.io/api-reference/media-api/list-and-search-files#advanced-search-queries).
+## Raw responses
-**2. Get File Details**
+The SDK defines methods that deserialize responses into instances of Java classes. However, these methods don't provide access to the response headers, status code, or the raw response body.
-Accepts the file ID and fetches the details as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-details)
+To access this data, prefix any HTTP method call on a client or service with `withRawResponse()`:
```java
-String fileId="your-file-id";
-Result result=ImageKit.getInstance().getFileDetail(fileId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+import com.imagekit.api.core.http.Headers;
+import com.imagekit.api.core.http.HttpResponseFor;
+import com.imagekit.api.models.files.FileUploadParams;
+import com.imagekit.api.models.files.FileUploadResponse;
+import java.io.ByteArrayInputStream;
-**3. Get File Versions**
+FileUploadParams params = FileUploadParams.builder()
+ .file(ByteArrayInputStream("https://www.example.com/public-url.jpg".getBytes()))
+ .fileName("file-name.jpg")
+ .build();
+HttpResponseFor response = client.files().withRawResponse().upload(params);
-Accepts the file ID and fetches the details as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-versions).
-
-```java
-String fileId = "62a04834c10d49825c6de9e8";
-ResultFileVersions resultFileVersions = ImageKit.getInstance().getFileVersions(fileId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultFileVersions);
-System.out.println("Raw Response:");
-System.out.println(resultFileVersions.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultFileVersions.getResponseMetaData().getMap());
+int statusCode = response.statusCode();
+Headers headers = response.headers();
```
-**4. Get File Version details**
-
-Accepts the file ID and version ID and fetches the details as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/get-file-version-details).
+You can still deserialize the response into an instance of a Java class if needed:
```java
-String fileId = "62a04834c10d49825c6de9e8";
-String versionId = "62a04834c10d49825c6de9e8";
-ResultFileVersionDetails resultFileVersionDetails = ImageKit.getInstance().getFileVersionDetails(fileId, versionId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultFileVersionDetails);
-System.out.println("Raw Response:");
-System.out.println(resultFileVersionDetails.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultFileVersionDetails.getResponseMetaData().getMap());
+import com.imagekit.api.models.files.FileUploadResponse;
+
+FileUploadResponse parsedResponse = response.parse();
```
-**5. Update File Details**
+## Error handling
-Accepts an object of class `FileUpdateRequest` specifying the parameters to be used to update file details. All parameters specified in the [documentation here] (https://docs.imagekit.io/api-reference/media-api/update-file-details) can be passed via their setter functions to get the results.
+The SDK throws custom unchecked exception types:
-```java
-List tags = new ArrayList<>();
-tags.add("Software");
-tags.add("Developer");
-tags.add("Engineer");
-
-List aiTags = new ArrayList<>();
-aiTags.add("Plant");
-FileUpdateRequest fileUpdateRequest = new FileUpdateRequest("fileId");
-fileUpdateRequest.setTags(tags);
-fileUpdateRequest.setRemoveAITags(aiTags);
-fileUpdateRequest.setWebhookUrl("https://webhook.site/c78d617f-33bc-40d9-9e61-608999721e2e");
-
-JsonObject optionsInnerObject = new JsonObject();
-optionsInnerObject.addProperty("add_shadow", true);
-optionsInnerObject.addProperty("bg_color", "yellow");
-JsonObject innerObject1 = new JsonObject();
-innerObject1.addProperty("name", "remove-bg");
-innerObject1.add("options", optionsInnerObject);
-JsonObject innerObject2 = new JsonObject();
-innerObject2.addProperty("name", "google-auto-tagging");
-innerObject2.addProperty("minConfidence", 15);
-innerObject2.addProperty("maxTags", 20);
-JsonArray jsonArray = new JsonArray();
-jsonArray.add(innerObject1);
-jsonArray.add(innerObject2);
-
-fileUpdateRequest.setExtensions(jsonArray);
-fileUpdateRequest.setCustomCoordinates("10,10,40,40");
-JsonObject jsonObjectCustomMetadata = new JsonObject();
-jsonObjectCustomMetadata.addProperty("test10", 11);
-fileUpdateRequest.setCustomMetadata(jsonObjectCustomMetadata);
-Result result=ImageKit.getInstance().updateFileDetail(fileUpdateRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+- [`ImageKitServiceException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/ImageKitServiceException.kt): Base class for HTTP errors. See this table for which exception subclass is thrown for each HTTP status code:
-**6. Add tags**
+ | Status | Exception |
+ | ------ | ------------------------------------------------------------------------------------------------------------------------------- |
+ | 400 | [`BadRequestException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/BadRequestException.kt) |
+ | 401 | [`UnauthorizedException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/UnauthorizedException.kt) |
+ | 403 | [`PermissionDeniedException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/PermissionDeniedException.kt) |
+ | 404 | [`NotFoundException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/NotFoundException.kt) |
+ | 422 | [`UnprocessableEntityException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/UnprocessableEntityException.kt) |
+ | 429 | [`RateLimitException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/RateLimitException.kt) |
+ | 5xx | [`InternalServerException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/InternalServerException.kt) |
+ | others | [`UnexpectedStatusCodeException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/UnexpectedStatusCodeException.kt) |
-Accepts an object of class `TagsRequest` specifying the parameters to be used to add tags. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/add-tags-bulk) can be passed via their setter functions to get the results.
+- [`ImageKitIoException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/ImageKitIoException.kt): I/O networking errors.
-```java
-List fileIds = new ArrayList<>();
-fileIds.add("FileId");
-List tags = new ArrayList<>();
-tags.add("tag-to-add-1");
-tags.add("tag-to-add-2");
-ResultTags resultTags=ImageKit.getInstance().addTags(new TagsRequest(fileIds, tags));
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultTags);
-System.out.println("Raw Response:");
-System.out.println(resultTags.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultTags.getResponseMetaData().getMap());
-```
+- [`ImageKitRetryableException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/ImageKitRetryableException.kt): Generic error indicating a failure that could be retried by the client.
-**7. Remove tags**
+- [`ImageKitInvalidDataException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/ImageKitInvalidDataException.kt): Failure to interpret successfully parsed data. For example, when accessing a property that's supposed to be required, but the API unexpectedly omitted it from the response.
-Accepts an object of class `TagsRequest` specifying the parameters to be used to remove tags. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/remove-tags-bulk) can be passed via their setter functions to get the results.
+- [`ImageKitException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/ImageKitException.kt): Base class for all exceptions. Most errors will result in one of the previously mentioned ones, but completely generic errors may be thrown using the base class.
-```java
-List fileIds = new ArrayList<>();
-fileIds.add("FileId");
-List tags = new ArrayList<>();
-tags.add("tag-to-remove-1");
-tags.add("tag-to-remove-2");
-ResultTags resultTags=ImageKit.getInstance().removeTags(new TagsRequest(fileIds, tags));
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultTags);
-System.out.println("Raw Response:");
-System.out.println(resultTags.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultTags.getResponseMetaData().getMap());
-```
+## Logging
-**8. Remove AI tags**
+The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
-Accepts an object of class `AITagsRequest` specifying the parameters to be used to remove AI tags. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/remove-aitags-bulk) can be passed via their setter functions to get the results.
+Enable logging by setting the `IMAGE_KIT_LOG` environment variable to `info`:
-```java
-List fileIds = new ArrayList<>();
-fileIds.add("629f3de17eb0fe4053615450");
-List aiTags = new ArrayList<>();
-aiTags.add("Rectangle");
-AITagsRequest aiTagsRequest =new AITagsRequest();
-aiTagsRequest.setFileIds(fileIds);
-aiTagsRequest.setAITags(aiTags);
-ResultTags resultTags = ImageKit.getInstance().removeAITags(aiTagsRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultTags);
-System.out.println("Raw Response:");
-System.out.println(resultTags.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultTags.getResponseMetaData().getMap());
+```sh
+$ export IMAGE_KIT_LOG=info
```
-**9. Delete File**
+Or to `debug` for more verbose logging:
-Accepts the file ID and delete a file as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file).
-
-```java
-String fileId="your-file-id";
-Result result=ImageKit.getInstance().deleteFile(fileId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
+```sh
+$ export IMAGE_KIT_LOG=debug
```
-**10. Delete FileVersion**
+## ProGuard and R8
-Accepts an object of class `DeleteFileVersionRequest` specifying the parameters to be used to delete file version. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/delete-file-version) can be passed via their setter functions to get the results.
+Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `image-kit-java-core` is published with a [configuration file](image-kit-java-core/src/main/resources/META-INF/proguard/image-kit-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).
-```java
-DeleteFileVersionRequest deleteFileVersionRequest = new DeleteFileVersionRequest();
-deleteFileVersionRequest.setFileId("629d95278482ba129fd17c97");
-deleteFileVersionRequest.setVersionId("629d953ebd24e8ceca911a66");
-ResultNoContent resultNoContent = ImageKit.getInstance().deleteFileVersion(deleteFileVersionRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultNoContent);
-System.out.println("Raw Response:");
-System.out.println(resultNoContent.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultNoContent.getResponseMetaData().getMap());
-```
+ProGuard and R8 should automatically detect and use the published rules, but you can also manually copy the keep rules if necessary.
-**11. Delete files (bulk)**
+## Jackson
-Accepts the file IDs to delete files as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/delete-files-bulk).
+The SDK depends on [Jackson](https://github.com/FasterXML/jackson) for JSON serialization/deserialization. It is compatible with version 2.13.4 or higher, but depends on version 2.18.2 by default.
-```java
-List fileIds = new ArrayList<>();
-fileIds.add("your-file-id");
-fileIds.add("your-file-id");
-fileIds.add("your-file-id");
-
-ResultFileDelete result=ImageKit.getInstance().bulkDeleteFiles(fileIds);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+The SDK throws an exception if it detects an incompatible Jackson version at runtime (e.g. if the default version was overridden in your Maven or Gradle config).
-**12. Copy file**
+If the SDK threw an exception, but you're _certain_ the version is compatible, then disable the version check using the `checkJacksonVersionCompatibility` on [`ImageKitOkHttpClient`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt) or [`ImageKitOkHttpClientAsync`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt).
-Accepts an object of class `CopyFileRequest` specifying the parameters to be used to copy file. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/copy-file) can be passed via their setter functions to get the results.
+> [!CAUTION]
+> We make no guarantee that the SDK works correctly when the Jackson version check is disabled.
-```java
-CopyFileRequest copyFileRequest = new CopyFileRequest();
-copyFileRequest.setSourceFilePath("/w2_image.png");
-copyFileRequest.setDestinationPath("/Gallery/");
-copyFileRequest.setIncludeFileVersions(true);
-ResultNoContent resultNoContent = ImageKit.getInstance().copyFile(copyFileRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultNoContent);
-System.out.println("Raw Response:");
-System.out.println(resultNoContent.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultNoContent.getResponseMetaData().getMap());
-```
+## Network options
-**13. Move file**
+### Retries
-Accepts an object of class `MoveFileRequest` specifying the parameters to be used to move file. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/move-file) can be passed via their setter functions to get the results.
+The SDK automatically retries 2 times by default, with a short exponential backoff between requests.
-```java
-MoveFileRequest moveFileRequest = new MoveFileRequest();
-moveFileRequest.setSourceFilePath("/Gallery/w2_image.png");
-moveFileRequest.setDestinationPath("/");
-ResultNoContent resultNoContent = ImageKit.getInstance().moveFile(moveFileRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultNoContent);
-System.out.println("Raw Response:");
-System.out.println(resultNoContent.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultNoContent.getResponseMetaData().getMap());
-```
+Only the following error types are retried:
+
+- Connection errors (for example, due to a network connectivity problem)
+- 408 Request Timeout
+- 409 Conflict
+- 429 Rate Limit
+- 5xx Internal
-**14. Rename file**
+The API may also explicitly instruct the SDK to retry or not retry a request.
-Accepts an object of class `RenameFileRequest` specifying the parameters to be used to rename file. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/rename-file) can be passed via their setter functions to get the results.
+To set a custom number of retries, configure the client using the `maxRetries` method:
```java
-RenameFileRequest renameFileRequest = new RenameFileRequest();
-renameFileRequest.setFilePath("/w2_image.png");
-renameFileRequest.setNewFileName("w2_image_s.png");
-renameFileRequest.setPurgeCache(true);
-ResultRenameFile resultRenameFile = ImageKit.getInstance().renameFile(renameFileRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultRenameFile);
-System.out.println("Raw Response:");
-System.out.println(resultRenameFile.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultRenameFile.getResponseMetaData().getMap());
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ .fromEnv()
+ .maxRetries(4)
+ .build();
```
-**15. Restore file Version**
+### Timeouts
-Accepts the fileId and versionId to restore file version as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/restore-file-version).
+Requests time out after 1 minute by default.
+
+To set a custom timeout, configure the method call using the `timeout` method:
```java
-Result result = ImageKit.getInstance().restoreFileVersion("fileId", "versionId");
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+import com.imagekit.api.models.files.FileUploadResponse;
-**16. Create Folder**
+FileUploadResponse response = client.files().upload(
+ params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()
+);
+```
-Accepts an object of class `CreateFolderRequest` specifying the parameters to be used to create folder. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/create-folder) can be passed via their setter functions to get the results.
+Or configure the default for all method calls at the client level:
```java
-CreateFolderRequest createFolderRequest = new CreateFolderRequest();
-createFolderRequest.setFolderName("test1");
-createFolderRequest.setParentFolderPath("/");
-ResultEmptyBlock resultEmptyBlock = ImageKit.getInstance().createFolder(createFolderRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultEmptyBlock);
-System.out.println("Raw Response:");
-System.out.println(resultEmptyBlock.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultEmptyBlock.getResponseMetaData().getMap());
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+import java.time.Duration;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ .fromEnv()
+ .timeout(Duration.ofSeconds(30))
+ .build();
```
-**17. Delete Folder**
+### Proxies
-Accepts an object of class `DeleteFolderRequest` specifying the parameters to be used to delete folder. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/delete-folder) can be passed via their setter functions to get the results.
+To route requests through a proxy, configure the client using the `proxy` method:
```java
-DeleteFolderRequest deleteFolderRequest = new DeleteFolderRequest();
-deleteFolderRequest.setFolderPath("/test1");
-ResultNoContent resultNoContent = ImageKit.getInstance().deleteFolder(deleteFolderRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultNoContent);
-System.out.println("Raw Response:");
-System.out.println(resultNoContent.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultNoContent.getResponseMetaData().getMap());
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ .fromEnv()
+ .proxy(new Proxy(
+ Proxy.Type.HTTP, new InetSocketAddress(
+ "https://example.com", 8080
+ )
+ ))
+ .build();
```
-**18. Copy Folder**
+### HTTPS
+
+> [!NOTE]
+> Most applications should not call these methods, and instead use the system defaults. The defaults include
+> special optimizations that can be lost if the implementations are modified.
-Accepts an object of class `CopyFolderRequest` specifying the parameters to be used to copy folder. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/copy-folder) can be passed via their setter functions to get the results.
+To configure how HTTPS connections are secured, configure the client using the `sslSocketFactory`, `trustManager`, and `hostnameVerifier` methods:
```java
-CopyFolderRequest copyFolderRequest = new CopyFolderRequest();
-copyFolderRequest.setSourceFolderPath("/Gallery/test");
-copyFolderRequest.setDestinationPath("/");
-ResultOfFolderActions resultOfFolderActions = ImageKit.getInstance().copyFolder(copyFolderRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultOfFolderActions);
-System.out.println("Raw Response:");
-System.out.println(resultOfFolderActions.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultOfFolderActions.getResponseMetaData().getMap());
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ .fromEnv()
+ // If `sslSocketFactory` is set, then `trustManager` must be set, and vice versa.
+ .sslSocketFactory(yourSSLSocketFactory)
+ .trustManager(yourTrustManager)
+ .hostnameVerifier(yourHostnameVerifier)
+ .build();
```
-**19. Move Folder**
+### Custom HTTP client
-Accepts an object of class `MoveFolderRequest` specifying the parameters to be used to move folder. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/media-api/move-folder) can be passed via their setter functions to get the results.
+The SDK consists of three artifacts:
-```java
-MoveFolderRequest moveFolderRequest = new MoveFolderRequest();
-moveFolderRequest.setSourceFolderPath("/Gallery/test");
-moveFolderRequest.setDestinationPath("/");
-ResultOfFolderActions resultOfFolderActions = ImageKit.getInstance().moveFolder(moveFolderRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultOfFolderActions);
-System.out.println("Raw Response:");
-System.out.println(resultOfFolderActions.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultOfFolderActions.getResponseMetaData().getMap());
-```
+- `image-kit-java-core`
+ - Contains core SDK logic
+ - Does not depend on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`ImageKitClient`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClient.kt), [`ImageKitClientAsync`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsync.kt), [`ImageKitClientImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt), and [`ImageKitClientAsyncImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt), all of which can work with any HTTP client
+- `image-kit-java-client-okhttp`
+ - Depends on [OkHttp](https://square.github.io/okhttp)
+ - Exposes [`ImageKitOkHttpClient`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt) and [`ImageKitOkHttpClientAsync`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt), which provide a way to construct [`ImageKitClientImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt) and [`ImageKitClientAsyncImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt), respectively, using OkHttp
+- `image-kit-java`
+ - Depends on and exposes the APIs of both `image-kit-java-core` and `image-kit-java-client-okhttp`
+ - Does not have its own logic
-**20. Get Bulk Job Status**
+This structure allows replacing the SDK's default HTTP client without pulling in unnecessary dependencies.
-Accepts the jobId to get bulk job status as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/copy-move-folder-status).
+#### Customized [`OkHttpClient`](https://square.github.io/okhttp/3.x/okhttp/okhttp3/OkHttpClient.html)
-```java
-String jobId = "629f44ac7eb0fe8173622d4b";
-ResultBulkJobStatus resultBulkJobStatus = ImageKit.getInstance().getBulkJobStatus(jobId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultBulkJobStatus);
-System.out.println("Raw Response:");
-System.out.println(resultBulkJobStatus.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultBulkJobStatus.getResponseMetaData().getMap());
-```
+> [!TIP]
+> Try the available [network options](#network-options) before replacing the default client.
-**21. Purge Cache**
+To use a customized `OkHttpClient`:
-Accepts a full URL of the file for which the cache has to be cleared as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache).
+1. Replace your [`image-kit-java` dependency](#installation) with `image-kit-java-core`
+2. Copy `image-kit-java-client-okhttp`'s [`OkHttpClient`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/OkHttpClient.kt) class into your code and customize it
+3. Construct [`ImageKitClientImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt) or [`ImageKitClientAsyncImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt), similarly to [`ImageKitOkHttpClient`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt) or [`ImageKitOkHttpClientAsync`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt), using your customized client
-```java
-ResultCache result=ImageKit.getInstance().purgeCache("https://ik.imagekit.io/imagekit-id/default-image.jpg");
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+### Completely custom HTTP client
-**22. Purge Cache Status**
+To use a completely custom HTTP client:
-Accepts a request ID and fetch purge cache status as per the [API documentation here](https://docs.imagekit.io/api-reference/media-api/purge-cache-status)
+1. Replace your [`image-kit-java` dependency](#installation) with `image-kit-java-core`
+2. Write a class that implements the [`HttpClient`](image-kit-java-core/src/main/kotlin/com/imagekit/api/core/http/HttpClient.kt) interface
+3. Construct [`ImageKitClientImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt) or [`ImageKitClientAsyncImpl`](image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt), similarly to [`ImageKitOkHttpClient`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt) or [`ImageKitOkHttpClientAsync`](image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt), using your new client class
-```java
-String requestId="cache-requestId";
-ResultCacheStatus result=ImageKit.getInstance().getPurgeCacheStatus(requestId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+## Undocumented API functionality
-**23. Get File Metadata**
+The SDK is typed for convenient usage of the documented API. However, it also supports working with undocumented or not yet supported parts of the API.
-Accepts the file ID and fetches the metadata as per the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-for-uploaded-media-files)
+### Parameters
-```java
-String fileId="your-file-id";
-ResultMetaData result=ImageKit.getInstance().getFileMetadata(fileId);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+To set undocumented parameters, call the `putAdditionalHeader`, `putAdditionalQueryParam`, or `putAdditionalBodyProperty` methods on any `Params` class:
-Another way to get metadata from a remote file URL as per the [API documentation here](https://docs.imagekit.io/api-reference/metadata-api/get-image-metadata-from-remote-url). This file should be accessible over the ImageKit.io URL-endpoint.
```java
-String url="Remote File URL";
-ResultMetaData result=ImageKit.getInstance().getRemoteFileMetadata(url);
-System.out.println("======FINAL RESULT=======");
-System.out.println(result);
-System.out.println("Raw Response:");
-System.out.println(result.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(result.getResponseMetaData().getMap());
-```
+import com.imagekit.api.core.JsonValue;
+import com.imagekit.api.models.files.FileUploadParams;
-**24. Create CustomMetaDataFields**
-
-Accepts an object of class `CustomMetaDataFieldCreateRequest` specifying the parameters to be used to create cusomMetaDataFields. All parameters specified in the [documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/create-custom-metadata-field) can be passed as-is with the correct values to get the results.
+FileUploadParams params = FileUploadParams.builder()
+ .putAdditionalHeader("Secret-Header", "42")
+ .putAdditionalQueryParam("secret_query_param", "42")
+ .putAdditionalBodyProperty("secretProperty", JsonValue.from("42"))
+ .build();
+```
-Check for the [Allowed Values In The Schema](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/create-custom-metadata-field#allowed-values-in-the-schema-object).
+These can be accessed on the built object later using the `_additionalHeaders()`, `_additionalQueryParams()`, and `_additionalBodyProperties()` methods.
-#### Examples:
+To set undocumented parameters on _nested_ headers, query params, or body classes, call the `putAdditionalProperty` method on the nested class:
```java
-CustomMetaDataFieldSchemaObject schemaObject = new CustomMetaDataFieldSchemaObject();
-schemaObject.setType("Number");
-schemaObject.setMinValue(10);
-schemaObject.setMaxValue(200);
-CustomMetaDataFieldCreateRequest customMetaDataFieldCreateRequest = new CustomMetaDataFieldCreateRequest();
-customMetaDataFieldCreateRequest.setName("Name");
-customMetaDataFieldCreateRequest.setLabel("Label");
-customMetaDataFieldCreateRequest.setSchema(schemaObject);
-ResultCustomMetaDataField resultCustomMetaDataField=ImageKit.getInstance().createCustomMetaDataFields(customMetaDataFieldCreateRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultCustomMetaDataField);
-System.out.println("Raw Response:");
-System.out.println(resultCustomMetaDataField.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultCustomMetaDataField.getResponseMetaData().getMap());
+import com.imagekit.api.core.JsonValue;
+import com.imagekit.api.models.files.FileUploadParams;
+
+FileUploadParams params = FileUploadParams.builder()
+ .transformation(FileUploadParams.Transformation.builder()
+ .putAdditionalProperty("secretProperty", JsonValue.from("42"))
+ .build())
+ .build();
```
-- MultiSelect type Exmample:
+These properties can be accessed on the nested built object later using the `_additionalProperties()` method.
+
+To set a documented parameter or property to an undocumented or not yet supported _value_, pass a [`JsonValue`](image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt) object to its setter:
```java
-List objectList = new ArrayList<>();
-objectList.add("small");
-objectList.add(30);
-objectList.add(40);
-objectList.add(true);
-
-List defaultValueObject = new ArrayList<>();
-defaultValueObject.add("small");
-defaultValueObject.add(30);
-defaultValueObject.add(true);
-CustomMetaDataFieldSchemaObject customMetaDataFieldSchemaObject = new CustomMetaDataFieldSchemaObject();
-customMetaDataFieldSchemaObject.setType("MultiSelect");
-customMetaDataFieldSchemaObject.setValueRequired(true); // optional
-customMetaDataFieldSchemaObject.setDefaultValue(defaultValueObject); // required if isValueRequired set to true
-customMetaDataFieldSchemaObject.setSelectOptions(objectList);
-CustomMetaDataFieldCreateRequest customMetaDataFieldCreateRequest = new CustomMetaDataFieldCreateRequest();
-customMetaDataFieldCreateRequest.setName("Name-MultiSelect");
-customMetaDataFieldCreateRequest.setLabel("Label-MultiSelect");
-customMetaDataFieldCreateRequest.setSchema(customMetaDataFieldSchemaObject);
-
-ResultCustomMetaDataField resultCustomMetaDataField = ImageKit.getInstance()
- .createCustomMetaDataFields(customMetaDataFieldCreateRequest);
+import com.imagekit.api.core.JsonValue;
+import com.imagekit.api.models.files.FileUploadParams;
+
+FileUploadParams params = FileUploadParams.builder()
+ .file(JsonValue.from(42))
+ .fileName("file-name.jpg")
+ .build();
```
-- Date type Exmample:
+The most straightforward way to create a [`JsonValue`](image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt) is using its `from(...)` method:
```java
-CustomMetaDataFieldSchemaObject customMetaDataFieldSchemaObject = new CustomMetaDataFieldSchemaObject();
-customMetaDataFieldSchemaObject.setType("Date");
-customMetaDataFieldSchemaObject.setValueRequired(true); // optional
-customMetaDataFieldSchemaObject.setDefaultValue("2022-11-30T10:11:10+00:00"); // required if isValueRequired set to true
-customMetaDataFieldSchemaObject.setMinValue("2022-11-30T10:11:10+00:00");
-customMetaDataFieldSchemaObject.setMaxValue("2022-12-30T10:11:10+00:00");
-
-CustomMetaDataFieldCreateRequest customMetaDataFieldCreateRequest = new CustomMetaDataFieldCreateRequest();
-customMetaDataFieldCreateRequest.setName("Name");
-customMetaDataFieldCreateRequest.setLabel("Label");
-customMetaDataFieldCreateRequest.setSchema(customMetaDataFieldSchemaObject);
-
-ResultCustomMetaDataField resultCustomMetaDataField = ImageKit.getInstance()
- .createCustomMetaDataFields(customMetaDataFieldCreateRequest);
-```
+import com.imagekit.api.core.JsonValue;
+import java.util.List;
+import java.util.Map;
+// Create primitive JSON values
+JsonValue nullValue = JsonValue.from(null);
+JsonValue booleanValue = JsonValue.from(true);
+JsonValue numberValue = JsonValue.from(42);
+JsonValue stringValue = JsonValue.from("Hello World!");
-**25. Get CustomMetaDataFields**
+// Create a JSON array value equivalent to `["Hello", "World"]`
+JsonValue arrayValue = JsonValue.from(List.of(
+ "Hello", "World"
+));
-Accepts the includeDeleted boolean and fetches the metadata as per the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/get-custom-metadata-field)
+// Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+JsonValue objectValue = JsonValue.from(Map.of(
+ "a", 1,
+ "b", 2
+));
-```java
-ResultCustomMetaDataFieldList resultCustomMetaDataFieldList=ImageKit.getInstance().getCustomMetaDataFields(false);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultCustomMetaDataFieldList);
-System.out.println("Raw Response:");
-System.out.println(resultCustomMetaDataFieldList.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultCustomMetaDataFieldList.getResponseMetaData().getList());
-System.out.println(resultCustomMetaDataFieldList.getResultCustomMetaDataFields());
+// Create an arbitrarily nested JSON equivalent to:
+// {
+// "a": [1, 2],
+// "b": [3, 4]
+// }
+JsonValue complexValue = JsonValue.from(Map.of(
+ "a", List.of(
+ 1, 2
+ ),
+ "b", List.of(
+ 3, 4
+ )
+));
```
-**26. Edit CustomMetaDataFields**
+Normally a `Builder` class's `build` method will throw [`IllegalStateException`](https://docs.oracle.com/javase/8/docs/api/java/lang/IllegalStateException.html) if any required parameter or property is unset.
-Accepts an ID of customMetaDataField and object of class `CustomMetaDataFieldUpdateRequest` specifying the parameters to be used to edit cusomMetaDataFields as per the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/update-custom-metadata-field).
+To forcibly omit a required parameter or property, pass [`JsonMissing`](image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt):
```java
-CustomMetaDataFieldSchemaObject schemaObject = new CustomMetaDataFieldSchemaObject();
-schemaObject.setMinValue(10);
-schemaObject.setMaxValue(200);
-
-CustomMetaDataFieldUpdateRequest customMetaDataFieldUpdateRequest = new CustomMetaDataFieldUpdateRequest();
-customMetaDataFieldUpdateRequest.setId("id");
-customMetaDataFieldUpdateRequest.setLabel("label");
-customMetaDataFieldUpdateRequest.setSchema(schemaObject);
-ResultCustomMetaDataField resultCustomMetaDataField=ImageKit.getInstance().updateCustomMetaDataFields(customMetaDataFieldUpdateRequest);
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultCustomMetaDataField);
-System.out.println("Raw Response:");
-System.out.println(resultCustomMetaDataField.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultCustomMetaDataField.getResponseMetaData().getMap());
+import com.imagekit.api.core.JsonMissing;
+import com.imagekit.api.models.files.FileUploadParams;
+
+FileUploadParams params = FileUploadParams.builder()
+ .fileName("fileName")
+ .file(JsonMissing.of())
+ .build();
```
-**27. Delete CustomMetaDataFields**
+### Response properties
-Accepts the id to delete the customMetaDataFields as per the [API documentation here](https://docs.imagekit.io/api-reference/custom-metadata-fields-api/delete-custom-metadata-field).
+To access undocumented response properties, call the `_additionalProperties()` method:
```java
-ResultNoContent resultNoContent=ImageKit.getInstance().deleteCustomMetaDataField("id");
-System.out.println("======FINAL RESULT=======");
-System.out.println(resultNoContent);
-System.out.println("Raw Response:");
-System.out.println(resultNoContent.getResponseMetaData().getRaw());
-System.out.println("Map Response:");
-System.out.println(resultNoContent.getResponseMetaData().getMap());
-```
+import com.imagekit.api.core.JsonValue;
+import java.util.Map;
-## Utility functions
+Map additionalProperties = client.files().upload(params)._additionalProperties();
+JsonValue secretPropertyValue = additionalProperties.get("secretProperty");
-We have included the following commonly used utility functions in this package.
+String result = secretPropertyValue.accept(new JsonValue.Visitor<>() {
+ @Override
+ public String visitNull() {
+ return "It's null!";
+ }
-**Authentication parameter generation**
+ @Override
+ public String visitBoolean(boolean value) {
+ return "It's a boolean!";
+ }
-In case you are looking to implement client-side file upload, you are going to need a token, expiry timestamp, and a valid signature for that upload. The SDK provides a simple method that you can use in your code to generate these
-authentication parameters for you.
+ @Override
+ public String visitNumber(Number value) {
+ return "It's a number!";
+ }
-Note: The Private API Key should never be exposed in any client-side code. You must always generate these authentications
- parameters on the server-side
+ // Other methods include `visitMissing`, `visitString`, `visitArray`, and `visitObject`
+ // The default implementation of each unimplemented method delegates to `visitDefault`, which throws by default, but can also be overridden
+});
+```
-authentication
+To access a property's raw JSON value, which may be undocumented, call its `_` prefixed method:
```java
-Map authenticationParams = ImageKit.getInstance().getAuthenticationParameters(token, expire);
-```
+import com.imagekit.api.core.JsonField;
+import java.io.InputStream;
+import java.util.Optional;
+
+JsonField file = client.files().upload(params)._file();
-Returns Map object of this json
-```json
-{
- "token": "unique_token",
- "expire": "valid_expiry_timestamp",
- "signature": "generated_signature"
+if (file.isMissing()) {
+ // The property is absent from the JSON response
+} else if (file.isNull()) {
+ // The property was set to literal null
+} else {
+ // Check if value was provided as a string
+ // Other methods include `asNumber()`, `asBoolean()`, etc.
+ Optional jsonString = file.asString();
+
+ // Try to deserialize into a custom type
+ MyClass myObject = file.asUnknown().orElseThrow().convert(MyClass.class);
}
```
-Both the `token` and `expire` parameters are optional. If not specified, the SDK uses the uuid to generate a random
-token and also generates a valid expiry timestamp internally. The value of the token and expire used to generate the
-signature are always returned in the response, no matter if they are provided as an input to this method or not.
-
-**Distance calculation between two pHash values**
+### Response validation
-Perceptual hashing allows you to construct a hash value that uniquely identifies an input image based on the contents
-of an image. [imagekit.io metadata API](https://docs.imagekit.io/api-reference/metadata-api) returns the pHash
-value of an image in the response. You can use this value to find a duplicate (or similar) images by calculating the distance between the two images.
+In rare cases, the API may return a response that doesn't match the expected type. For example, the SDK may expect a property to contain a `String`, but the API could return something else.
+By default, the SDK will not throw an exception in this case. It will throw [`ImageKitInvalidDataException`](image-kit-java-core/src/main/kotlin/com/imagekit/api/errors/ImageKitInvalidDataException.kt) only if you directly access the property.
-This SDK exposes phash_distance function to calculate the distance between two pHash value. It accepts two pHash hexadecimal
-strings and returns a numeric value indicative of the level of difference between the two images.
+If you would prefer to check that the response is completely well-typed upfront, then either call `validate()`:
```java
-int calculateDistance(){
- // fetch metadata of two uploaded image files
- ...
- // extract pHash strings from both: say 'first_hash' and 'second_hash'
- ...
- // calculate the distance between them:
-
- int distance = ImageKit.getInstance().pHashDistance(first_hash, second_hash);
- return distance;
-}
+import com.imagekit.api.models.files.FileUploadResponse;
+
+FileUploadResponse response = client.files().upload(params).validate();
```
-**Distance calculation examples**
-```java
-ImageKit.getInstance().pHashDistance("f06830ca9f1e3e90", "f06830ca9f1e3e90");
-// output: 0 (ame image)
+Or configure the method call to validate the response using the `responseValidation` method:
-ImageKit.getInstance().pHashDistance("2d5ad3936d2e015b", "2d6ed293db36a4fb");
-// output: 17 (similar images)
+```java
+import com.imagekit.api.models.files.FileUploadResponse;
-ImageKit.getInstance().pHashDistance("a4a65595ac94518b", "7838873e791f8400");
-// output: 37 (dissimilar images)
+FileUploadResponse response = client.files().upload(
+ params, RequestOptions.builder().responseValidation(true).build()
+);
```
-**HTTP response metadata of Internal API**
-
-HTTP response metadata of the internal API call can be accessed using the getResponseMetaData function on the Result (or ResultList, ResultCache etc.) object. Example:
+Or configure the default for all method calls at the client level:
```java
-Result result = ImageKit.getInstance().upload(fileCreateRequest);
-result.getResponseMetaData().getRaw();
-result.getResponseMetaData().getHeaders();
-result.getResponseMetaData().getHttpStatusCode();
+import com.imagekit.api.client.ImageKitClient;
+import com.imagekit.api.client.okhttp.ImageKitOkHttpClient;
+
+ImageKitClient client = ImageKitOkHttpClient.builder()
+ .fromEnv()
+ .responseValidation(true)
+ .build();
```
-## Sample Code Instruction
+## FAQ
-**1. First clone this repository to your system using git.**
-```shell script
-git clone https://github.com/imagekit-developer/imagekit-java.git
-```
-**2. Open project in your favorite Java IDE that can supports Gradle dependency management or you can use Terminal/Command Prompt.**
+### Why don't you use plain `enum` classes?
-**3. Goto `src/main/resources` directory.**
+Java `enum` classes are not trivially [forwards compatible](https://www.stainless.com/blog/making-java-enums-forwards-compatible). Using them in the SDK could cause runtime exceptions if the API is updated to respond with a new enum value.
-**4. Rename file `config.sample.properties` to `config.properties`.**
+### Why do you represent fields using `JsonField` instead of just plain `T`?
-**5. Edit `config.properties` and write values of given keys.**
-```properties
-UrlEndpoint=your_url_endpoint
-PrivateKey=your_private_key
-PublicKey=your_public_key
-```
+Using `JsonField` enables a few features:
-**5. You will find `App.java` in `src/main/java/io/imagekit/sampleapp/` directory. Edit program as you need, then run `App.java`. If you are using CLI Tool (Terminal/Command Prompt) Then Open Project in CLI and execute using gradle**
-```shell
-cd imagekit-java
-./gradlew run
-```
-* Run test case:
-```shell
-./gradlew imagekit-sdk:test
-```
-* Build ImageKit SDK:
-```shell
-./gradlew imagekit-sdk:clean
-./gradlew imagekit-sdk:build
-# You will find jar in "imagekit-sdk/build/libs/" directory.
-```
+- Allowing usage of [undocumented API functionality](#undocumented-api-functionality)
+- Lazily [validating the API response against the expected shape](#response-validation)
+- Representing absent vs explicitly null values
-## Handling errors
-Catch and respond to invalid data, internal problems, and more.
+### Why don't you use [`data` classes](https://kotlinlang.org/docs/data-classes.html)?
-Imagekit Java SDK raise exceptions for many reasons, such as not found, invalid parameters, authentication errors, and internal server error. We recommend writing code that gracefully handles all possible API exceptions.
+It is not [backwards compatible to add new fields to a data class](https://kotlinlang.org/docs/api-guidelines-backward-compatibility.html#avoid-using-data-classes-in-your-api) and we don't want to introduce a breaking change every time we add a field to a class.
-#### Example:
+### Why don't you use checked exceptions?
-```java
-try {
- // Use ImageKit's SDK to make requests...
-} catch (BadRequestException e) {
- // Missing or Invalid parameters were supplied to Imagekit.io's API
- System.out.println("Status is: " + e.getResponseMetaData().getHttpStatusCode());
- System.out.println("Message is: " + e.getMessage());
- System.out.println("Headers are: " + e.getResponseMetaData().getHeaders());
- System.out.println("Raw body is: " + e.getResponseMetaData().getRaw());
- System.out.println("Mapped body is: " + e.getResponseMetaData().getMap());
-} catch (UnauthorizedException e) {
- // No valid API key was provided.
-} catch (ForbiddenException e) {
- // Can be for the following reasons:
- // ImageKit could not authenticate your account with the keys provided.
- // An expired key (public or private) was used with the request.
- // The account is disabled.
- // If you are using the upload API, the total storage limit (or upload limit) is exceeded.
-} catch (TooManyRequestsException e) {
- // Too many requests made to the API too quickly
-} catch (InternalServerException e) {
- // Something went wrong with ImageKit.io API.
-} catch (PartialSuccessException e) {
- // Error cases on partial success.
-} catch (NotFoundException e) {
- // If any of the field or parameter is not found in data
-} catch (UnknownException e) {
- // Something else happened, which can be unrelated to imagekit, reason will be indicated in the message field
-}
-```
+Checked exceptions are widely considered a mistake in the Java programming language. In fact, they were omitted from Kotlin for this reason.
+
+Checked exceptions:
-## Supporttim
-For any feedback or to report any issues or general implementation support, please reach out to [support@imagekit.io]()
+- Are verbose to handle
+- Encourage error handling at the wrong level of abstraction, where nothing can be done about the error
+- Are tedious to propagate due to the [function coloring problem](https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function)
+- Don't play well with lambdas (also due to the function coloring problem)
+## Semantic versioning
-## Links
+This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions:
-* [Documentation](https://docs.imagekit.io/)
-* [Main Website](https://imagekit.io/)
+1. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_
+2. Changes that we do not expect to impact the vast majority of users in practice.
+We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience.
-## License
-Released under the MIT license.
+We are keen for your feedback; please open an [issue](https://www.github.com/imagekit-developer/imagekit-java/issues) with questions, bugs, or suggestions.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..8e64327
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,27 @@
+# Security Policy
+
+## Reporting Security Issues
+
+This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
+
+To report a security issue, please contact the Stainless team at security@stainless.com.
+
+## Responsible Disclosure
+
+We appreciate the efforts of security researchers and individuals who help us maintain the security of
+SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
+disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
+before making any information public.
+
+## Reporting Non-SDK Related Security Issues
+
+If you encounter security issues that are not directly related to SDKs but pertain to the services
+or products provided by Image Kit, please follow the respective company's security reporting guidelines.
+
+### Image Kit Terms and Policies
+
+Please contact developer@imagekit.io for any questions or concerns regarding the security of our services.
+
+---
+
+Thank you for helping us keep the SDKs and systems they interact with secure.
diff --git a/bin/check-release-environment b/bin/check-release-environment
new file mode 100644
index 0000000..3a6a7b4
--- /dev/null
+++ b/bin/check-release-environment
@@ -0,0 +1,33 @@
+#!/usr/bin/env bash
+
+errors=()
+
+if [ -z "${SONATYPE_USERNAME}" ]; then
+ errors+=("The SONATYPE_USERNAME secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${SONATYPE_PASSWORD}" ]; then
+ errors+=("The SONATYPE_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${GPG_SIGNING_KEY}" ]; then
+ errors+=("The GPG_SIGNING_KEY secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+if [ -z "${GPG_SIGNING_PASSWORD}" ]; then
+ errors+=("The GPG_SIGNING_PASSWORD secret has not been set. Please set it in either this repository's secrets or your organization secrets")
+fi
+
+lenErrors=${#errors[@]}
+
+if [[ lenErrors -gt 0 ]]; then
+ echo -e "Found the following errors in the release environment:\n"
+
+ for error in "${errors[@]}"; do
+ echo -e "- $error\n"
+ done
+
+ exit 1
+fi
+
+echo "The environment is ready to push releases!"
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..492c480
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,49 @@
+plugins {
+ id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
+ id("org.jetbrains.dokka") version "2.0.0"
+}
+
+repositories {
+ mavenCentral()
+}
+
+allprojects {
+ group = "com.imagekit.api"
+ version = "0.1.0" // x-release-please-version
+}
+
+subprojects {
+ // These are populated with dependencies by `buildSrc` scripts.
+ tasks.register("format") {
+ group = "Verification"
+ description = "Formats all source files."
+ }
+ tasks.register("lint") {
+ group = "Verification"
+ description = "Verifies all source files are formatted."
+ }
+ apply(plugin = "org.jetbrains.dokka")
+}
+
+subprojects {
+ apply(plugin = "org.jetbrains.dokka")
+}
+
+// Avoid race conditions between `dokkaJavadocCollector` and `dokkaJavadocJar` tasks
+tasks.named("dokkaJavadocCollector").configure {
+ subprojects.flatMap { it.tasks }
+ .filter { it.project.name != "image-kit-java" && it.name == "dokkaJavadocJar" }
+ .forEach { mustRunAfter(it) }
+}
+
+nexusPublishing {
+ repositories {
+ sonatype {
+ nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
+ snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
+
+ username.set(System.getenv("SONATYPE_USERNAME"))
+ password.set(System.getenv("SONATYPE_PASSWORD"))
+ }
+ }
+}
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 0000000..0b14135
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,12 @@
+plugins {
+ `kotlin-dsl`
+ kotlin("jvm") version "1.9.20"
+}
+
+repositories {
+ gradlePluginPortal()
+}
+
+dependencies {
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
+}
diff --git a/buildSrc/src/main/kotlin/image-kit.java.gradle.kts b/buildSrc/src/main/kotlin/image-kit.java.gradle.kts
new file mode 100644
index 0000000..81d5d32
--- /dev/null
+++ b/buildSrc/src/main/kotlin/image-kit.java.gradle.kts
@@ -0,0 +1,136 @@
+import org.gradle.api.tasks.testing.logging.TestExceptionFormat
+
+plugins {
+ `java-library`
+}
+
+repositories {
+ mavenCentral()
+}
+
+configure {
+ withJavadocJar()
+ withSourcesJar()
+}
+
+java {
+ toolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+}
+
+tasks.withType().configureEach {
+ options.compilerArgs.add("-Werror")
+ options.release.set(8)
+}
+
+tasks.named("javadocJar") {
+ setZip64(true)
+}
+
+tasks.named("jar") {
+ manifest {
+ attributes(mapOf(
+ "Implementation-Title" to project.name,
+ "Implementation-Version" to project.version
+ ))
+ }
+}
+
+tasks.withType().configureEach {
+ useJUnitPlatform()
+
+ // Run tests in parallel to some degree.
+ maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
+ forkEvery = 100
+
+ testLogging {
+ exceptionFormat = TestExceptionFormat.FULL
+ }
+}
+
+val palantir by configurations.creating
+dependencies {
+ palantir("com.palantir.javaformat:palantir-java-format:2.73.0")
+}
+
+fun registerPalantir(
+ name: String,
+ description: String,
+) {
+ val javaName = "${name}Java"
+ tasks.register(javaName) {
+ group = "Verification"
+ this.description = description
+
+ classpath = palantir
+ mainClass = "com.palantir.javaformat.java.Main"
+
+ // Avoid an `IllegalAccessError` on Java 9+.
+ jvmArgs(
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED",
+ "--add-exports", "jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED",
+ )
+
+ // Use paths relative to the current module.
+ val argumentFile =
+ project.layout.buildDirectory.file("palantir-$name-args.txt").get().asFile
+ val lastRunTimeFile =
+ project.layout.buildDirectory.file("palantir-$name-last-run.txt").get().asFile
+
+ // Read the time when this task was last executed for this module (if ever).
+ val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+ // Use a `fileTree` relative to the module's source directory.
+ val javaFiles = project.fileTree("src") { include("**/*.java") }
+
+ // Determine if any files need to be formatted or linted and continue only if there is at least
+ // one file.
+ onlyIf { javaFiles.any { it.lastModified() > lastRunTime } }
+
+ inputs.files(javaFiles)
+
+ doFirst {
+ // Create the argument file and set the preferred formatting style.
+ argumentFile.parentFile.mkdirs()
+ argumentFile.writeText("--palantir\n")
+
+ if (name == "lint") {
+ // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+ // the default 0) if any files need to be formatted, indicating that linting has failed.
+ argumentFile.appendText("--dry-run\n")
+ argumentFile.appendText("--set-exit-if-changed\n")
+ } else {
+ // `--dry-run` and `--replace` (for in-place formatting) are mutually exclusive.
+ argumentFile.appendText("--replace\n")
+ }
+
+ // Write the modified files to the argument file.
+ javaFiles.filter { it.lastModified() > lastRunTime }
+ .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+ }
+
+ doLast {
+ // Record the last execution time for later up-to-date checking.
+ lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+ }
+
+ // Pass the argument file using the @ symbol
+ args = listOf("@${argumentFile.absolutePath}")
+
+ outputs.upToDateWhen { javaFiles.none { it.lastModified() > lastRunTime } }
+ }
+
+ tasks.named(name) {
+ dependsOn(tasks.named(javaName))
+ }
+}
+
+registerPalantir(name = "format", description = "Formats all Java source files.")
+registerPalantir(name = "lint", description = "Verifies all Java source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/image-kit.kotlin.gradle.kts b/buildSrc/src/main/kotlin/image-kit.kotlin.gradle.kts
new file mode 100644
index 0000000..aaec88f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/image-kit.kotlin.gradle.kts
@@ -0,0 +1,106 @@
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
+
+plugins {
+ id("image-kit.java")
+ kotlin("jvm")
+}
+
+repositories {
+ mavenCentral()
+}
+
+kotlin {
+ jvmToolchain {
+ languageVersion.set(JavaLanguageVersion.of(21))
+ }
+
+ compilerOptions {
+ freeCompilerArgs = listOf(
+ "-Xjvm-default=all",
+ "-Xjdk-release=1.8",
+ // Suppress deprecation warnings because we may still reference and test deprecated members.
+ // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+.
+ "-nowarn",
+ )
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ languageVersion.set(KotlinVersion.KOTLIN_1_8)
+ apiVersion.set(KotlinVersion.KOTLIN_1_8)
+ coreLibrariesVersion = "1.8.0"
+ }
+}
+
+tasks.withType().configureEach {
+ systemProperty("junit.jupiter.execution.parallel.enabled", true)
+ systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent")
+}
+
+val ktfmt by configurations.creating
+dependencies {
+ ktfmt("com.facebook:ktfmt:0.56")
+}
+
+fun registerKtfmt(
+ name: String,
+ description: String,
+) {
+ val kotlinName = "${name}Kotlin"
+ tasks.register(kotlinName) {
+ group = "Verification"
+ this.description = description
+
+ classpath = ktfmt
+ mainClass = "com.facebook.ktfmt.cli.Main"
+
+ // Use paths relative to the current module.
+ val argumentFile = project.layout.buildDirectory.file("ktfmt-$name-args.txt").get().asFile
+ val lastRunTimeFile =
+ project.layout.buildDirectory.file("ktfmt-$name-last-run.txt").get().asFile
+
+ // Read the time when this task was last executed for this module (if ever).
+ val lastRunTime = lastRunTimeFile.takeIf { it.exists() }?.readText()?.toLongOrNull() ?: 0L
+
+ // Use a `fileTree` relative to the module's source directory.
+ val kotlinFiles = project.fileTree("src") { include("**/*.kt") }
+
+ // Determine if any files need to be formatted or linted and continue only if there is at least
+ // one file (otherwise Ktfmt will fail).
+ onlyIf { kotlinFiles.any { it.lastModified() > lastRunTime } }
+
+ inputs.files(kotlinFiles)
+
+ doFirst {
+ // Create the argument file and set the preferred formatting style.
+ argumentFile.parentFile.mkdirs()
+ argumentFile.writeText("--kotlinlang-style\n")
+
+ if (name == "lint") {
+ // For lint, do a dry run, so no files are modified. Set the exit code to 1 (instead of
+ // the default 0) if any files need to be formatted, indicating that linting has failed.
+ argumentFile.appendText("--dry-run\n")
+ argumentFile.appendText("--set-exit-if-changed\n")
+ }
+
+ // Write the modified files to the argument file.
+ kotlinFiles.filter { it.lastModified() > lastRunTime }
+ .forEach { argumentFile.appendText("${it.absolutePath}\n") }
+ }
+
+ doLast {
+ // Record the last execution time for later up-to-date checking.
+ lastRunTimeFile.writeText(System.currentTimeMillis().toString())
+ }
+
+ // Pass the argument file using the @ symbol
+ args = listOf("@${argumentFile.absolutePath}")
+
+ outputs.upToDateWhen { kotlinFiles.none { it.lastModified() > lastRunTime } }
+ }
+
+ tasks.named(name) {
+ dependsOn(tasks.named(kotlinName))
+ }
+}
+
+registerKtfmt(name = "format", description = "Formats all Kotlin source files.")
+registerKtfmt(name = "lint", description = "Verifies all Kotlin source files are formatted.")
diff --git a/buildSrc/src/main/kotlin/image-kit.publish.gradle.kts b/buildSrc/src/main/kotlin/image-kit.publish.gradle.kts
new file mode 100644
index 0000000..8d98781
--- /dev/null
+++ b/buildSrc/src/main/kotlin/image-kit.publish.gradle.kts
@@ -0,0 +1,61 @@
+plugins {
+ `maven-publish`
+ signing
+}
+
+configure {
+ publications {
+ register("maven") {
+ from(components["java"])
+
+ pom {
+ name.set("ImageKit API")
+ description.set("Checkout [API overview](/docs/api-overview) to learn about ImageKit's APIs,\nauthentication, rate limits, and error codes etc.")
+ url.set("https://imagekit.io/docs")
+
+ licenses {
+ license {
+ name.set("Apache-2.0")
+ }
+ }
+
+ developers {
+ developer {
+ name.set("Image Kit")
+ email.set("developer@imagekit.io")
+ }
+ }
+
+ scm {
+ connection.set("scm:git:git://github.com/imagekit-developer/imagekit-java.git")
+ developerConnection.set("scm:git:git://github.com/imagekit-developer/imagekit-java.git")
+ url.set("https://github.com/imagekit-developer/imagekit-java")
+ }
+
+ versionMapping {
+ allVariants {
+ fromResolutionResult()
+ }
+ }
+ }
+ }
+ }
+}
+
+signing {
+ val signingKeyId = System.getenv("GPG_SIGNING_KEY_ID")?.ifBlank { null }
+ val signingKey = System.getenv("GPG_SIGNING_KEY")?.ifBlank { null }
+ val signingPassword = System.getenv("GPG_SIGNING_PASSWORD")?.ifBlank { null }
+ if (signingKey != null && signingPassword != null) {
+ useInMemoryPgpKeys(
+ signingKeyId,
+ signingKey,
+ signingPassword,
+ )
+ sign(publishing.publications["maven"])
+ }
+}
+
+tasks.named("publish") {
+ dependsOn(":closeAndReleaseSonatypeStagingRepository")
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..6680f9c
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+org.gradle.caching=true
+org.gradle.configuration-cache=true
+org.gradle.parallel=true
+org.gradle.daemon=false
+# These options improve our compilation and test performance. They are inherited by the Kotlin daemon.
+org.gradle.jvmargs=\
+ -Xms2g \
+ -Xmx8g \
+ -XX:+UseParallelGC \
+ -XX:InitialCodeCacheSize=256m \
+ -XX:ReservedCodeCacheSize=1G \
+ -XX:MetaspaceSize=512m \
+ -XX:MaxMetaspaceSize=2G \
+ -XX:TieredStopAtLevel=1 \
+ -XX:GCTimeRatio=4 \
+ -XX:CICompilerCount=4 \
+ -XX:+OptimizeStringConcat \
+ -XX:+UseStringDeduplication
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 87b738c..a4b76b9 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index e9e7c76..cea7a79 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,7 @@
-#Sat Jan 04 19:50:00 IST 2020
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
index af6708f..f3b75f3 100755
--- a/gradlew
+++ b/gradlew
@@ -1,78 +1,129 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m"'
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +132,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
fi
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 0f8d593..9d21a21 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -1,84 +1,94 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windows variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/image-kit-java-client-okhttp/build.gradle.kts b/image-kit-java-client-okhttp/build.gradle.kts
new file mode 100644
index 0000000..cd001ec
--- /dev/null
+++ b/image-kit-java-client-okhttp/build.gradle.kts
@@ -0,0 +1,14 @@
+plugins {
+ id("image-kit.kotlin")
+ id("image-kit.publish")
+}
+
+dependencies {
+ api(project(":image-kit-java-core"))
+
+ implementation("com.squareup.okhttp3:okhttp:4.12.0")
+ implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
+
+ testImplementation(kotlin("test"))
+ testImplementation("org.assertj:assertj-core:3.25.3")
+}
diff --git a/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt b/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt
new file mode 100644
index 0000000..2da16e7
--- /dev/null
+++ b/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClient.kt
@@ -0,0 +1,327 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.client.okhttp
+
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.imagekit.api.client.ImageKitClient
+import com.imagekit.api.client.ImageKitClientImpl
+import com.imagekit.api.core.ClientOptions
+import com.imagekit.api.core.Timeout
+import com.imagekit.api.core.http.Headers
+import com.imagekit.api.core.http.HttpClient
+import com.imagekit.api.core.http.QueryParams
+import com.imagekit.api.core.jsonMapper
+import java.net.Proxy
+import java.time.Clock
+import java.time.Duration
+import java.util.Optional
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+
+/**
+ * A class that allows building an instance of [ImageKitClient] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
+class ImageKitOkHttpClient private constructor() {
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [ImageKitClient]. */
+ @JvmStatic fun builder() = Builder()
+
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ @JvmStatic fun fromEnv(): ImageKitClient = builder().fromEnv().build()
+ }
+
+ /** A builder for [ImageKitOkHttpClient]. */
+ class Builder internal constructor() {
+
+ private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
+
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
+ }
+
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.imagekit.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.imagekit.io`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ /**
+ * Your ImageKit private API key (it starts with `private_`). You can view and manage API
+ * keys in the [dashboard](https://imagekit.io/dashboard/developer/api-keys).
+ */
+ fun privateApiKey(privateApiKey: String) = apply {
+ clientOptions.privateApiKey(privateApiKey)
+ }
+
+ /**
+ * ImageKit Basic Auth only uses the username field and ignores the password. This field is
+ * unused.
+ *
+ * Defaults to `"do_not_set"`.
+ */
+ fun password(password: String?) = apply { clientOptions.password(password) }
+
+ /** Alias for calling [Builder.password] with `password.orElse(null)`. */
+ fun password(password: Optional) = password(password.getOrNull())
+
+ /**
+ * Your ImageKit webhook secret. This is used by the SDK to verify webhook signatures. It
+ * starts with a `whsec_` prefix. You can view and manage your webhook secret in the
+ * [dashboard](https://imagekit.io/dashboard/developer/webhooks). Treat the secret like a
+ * password, keep it private and do not expose it publicly. Learn more about
+ * [webhook verification](https://imagekit.io/docs/webhooks#verify-webhook-signature).
+ */
+ fun webhookSecret(webhookSecret: String?) = apply {
+ clientOptions.webhookSecret(webhookSecret)
+ }
+
+ /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */
+ fun webhookSecret(webhookSecret: Optional) =
+ webhookSecret(webhookSecret.getOrNull())
+
+ fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
+
+ fun headers(headers: Map>) = apply {
+ clientOptions.headers(headers)
+ }
+
+ fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply {
+ clientOptions.putHeaders(name, values)
+ }
+
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ clientOptions.putAllHeaders(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply {
+ clientOptions.replaceHeaders(name, value)
+ }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceHeaders(name, values)
+ }
+
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ clientOptions.replaceAllHeaders(headers)
+ }
+
+ fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+
+ fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
+ fun queryParams(queryParams: Map>) = apply {
+ clientOptions.queryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply {
+ clientOptions.putQueryParam(key, value)
+ }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.putQueryParams(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ clientOptions.replaceQueryParams(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.replaceQueryParams(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
+
+ fun removeAllQueryParams(keys: Set) = apply {
+ clientOptions.removeAllQueryParams(keys)
+ }
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ fun fromEnv() = apply { clientOptions.fromEnv() }
+
+ /**
+ * Returns an immutable instance of [ImageKitClient].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): ImageKitClient =
+ ImageKitClientImpl(
+ clientOptions
+ .httpClient(
+ OkHttpClient.builder()
+ .timeout(clientOptions.timeout())
+ .proxy(proxy)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
+ .build()
+ )
+ .build()
+ )
+ }
+}
diff --git a/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt b/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt
new file mode 100644
index 0000000..d998ad8
--- /dev/null
+++ b/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/ImageKitOkHttpClientAsync.kt
@@ -0,0 +1,327 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.client.okhttp
+
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.imagekit.api.client.ImageKitClientAsync
+import com.imagekit.api.client.ImageKitClientAsyncImpl
+import com.imagekit.api.core.ClientOptions
+import com.imagekit.api.core.Timeout
+import com.imagekit.api.core.http.Headers
+import com.imagekit.api.core.http.HttpClient
+import com.imagekit.api.core.http.QueryParams
+import com.imagekit.api.core.jsonMapper
+import java.net.Proxy
+import java.time.Clock
+import java.time.Duration
+import java.util.Optional
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import kotlin.jvm.optionals.getOrNull
+
+/**
+ * A class that allows building an instance of [ImageKitClientAsync] with [OkHttpClient] as the
+ * underlying [HttpClient].
+ */
+class ImageKitOkHttpClientAsync private constructor() {
+
+ companion object {
+
+ /** Returns a mutable builder for constructing an instance of [ImageKitClientAsync]. */
+ @JvmStatic fun builder() = Builder()
+
+ /**
+ * Returns a client configured using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ @JvmStatic fun fromEnv(): ImageKitClientAsync = builder().fromEnv().build()
+ }
+
+ /** A builder for [ImageKitOkHttpClientAsync]. */
+ class Builder internal constructor() {
+
+ private var clientOptions: ClientOptions.Builder = ClientOptions.builder()
+ private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */
+ fun proxy(proxy: Optional) = proxy(proxy.getOrNull())
+
+ /**
+ * The socket factory used to secure HTTPS connections.
+ *
+ * If this is set, then [trustManager] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ /** Alias for calling [Builder.sslSocketFactory] with `sslSocketFactory.orElse(null)`. */
+ fun sslSocketFactory(sslSocketFactory: Optional) =
+ sslSocketFactory(sslSocketFactory.getOrNull())
+
+ /**
+ * The trust manager used to secure HTTPS connections.
+ *
+ * If this is set, then [sslSocketFactory] must also be set.
+ *
+ * If unset, then the system default is used. Most applications should not call this method,
+ * and instead use the system default. The default include special optimizations that can be
+ * lost if the implementation is modified.
+ */
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ /** Alias for calling [Builder.trustManager] with `trustManager.orElse(null)`. */
+ fun trustManager(trustManager: Optional) =
+ trustManager(trustManager.getOrNull())
+
+ /**
+ * The verifier used to confirm that response certificates apply to requested hostnames for
+ * HTTPS connections.
+ *
+ * If unset, then a default hostname verifier is used.
+ */
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ /** Alias for calling [Builder.hostnameVerifier] with `hostnameVerifier.orElse(null)`. */
+ fun hostnameVerifier(hostnameVerifier: Optional) =
+ hostnameVerifier(hostnameVerifier.getOrNull())
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ clientOptions.checkJacksonVersionCompatibility(checkJacksonVersionCompatibility)
+ }
+
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.imagekit.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { clientOptions.jsonMapper(jsonMapper) }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { clientOptions.clock(clock) }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.imagekit.io`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { clientOptions.baseUrl(baseUrl) }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ clientOptions.responseValidation(responseValidation)
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = apply { clientOptions.timeout(timeout) }
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
+
+ /**
+ * Your ImageKit private API key (it starts with `private_`). You can view and manage API
+ * keys in the [dashboard](https://imagekit.io/dashboard/developer/api-keys).
+ */
+ fun privateApiKey(privateApiKey: String) = apply {
+ clientOptions.privateApiKey(privateApiKey)
+ }
+
+ /**
+ * ImageKit Basic Auth only uses the username field and ignores the password. This field is
+ * unused.
+ *
+ * Defaults to `"do_not_set"`.
+ */
+ fun password(password: String?) = apply { clientOptions.password(password) }
+
+ /** Alias for calling [Builder.password] with `password.orElse(null)`. */
+ fun password(password: Optional) = password(password.getOrNull())
+
+ /**
+ * Your ImageKit webhook secret. This is used by the SDK to verify webhook signatures. It
+ * starts with a `whsec_` prefix. You can view and manage your webhook secret in the
+ * [dashboard](https://imagekit.io/dashboard/developer/webhooks). Treat the secret like a
+ * password, keep it private and do not expose it publicly. Learn more about
+ * [webhook verification](https://imagekit.io/docs/webhooks#verify-webhook-signature).
+ */
+ fun webhookSecret(webhookSecret: String?) = apply {
+ clientOptions.webhookSecret(webhookSecret)
+ }
+
+ /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */
+ fun webhookSecret(webhookSecret: Optional) =
+ webhookSecret(webhookSecret.getOrNull())
+
+ fun headers(headers: Headers) = apply { clientOptions.headers(headers) }
+
+ fun headers(headers: Map>) = apply {
+ clientOptions.headers(headers)
+ }
+
+ fun putHeader(name: String, value: String) = apply { clientOptions.putHeader(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply {
+ clientOptions.putHeaders(name, values)
+ }
+
+ fun putAllHeaders(headers: Headers) = apply { clientOptions.putAllHeaders(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ clientOptions.putAllHeaders(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply {
+ clientOptions.replaceHeaders(name, value)
+ }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ clientOptions.replaceHeaders(name, values)
+ }
+
+ fun replaceAllHeaders(headers: Headers) = apply { clientOptions.replaceAllHeaders(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ clientOptions.replaceAllHeaders(headers)
+ }
+
+ fun removeHeaders(name: String) = apply { clientOptions.removeHeaders(name) }
+
+ fun removeAllHeaders(names: Set) = apply { clientOptions.removeAllHeaders(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply { clientOptions.queryParams(queryParams) }
+
+ fun queryParams(queryParams: Map>) = apply {
+ clientOptions.queryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply {
+ clientOptions.putQueryParam(key, value)
+ }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.putQueryParams(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.putAllQueryParams(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ clientOptions.replaceQueryParams(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ clientOptions.replaceQueryParams(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ clientOptions.replaceAllQueryParams(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { clientOptions.removeQueryParams(key) }
+
+ fun removeAllQueryParams(keys: Set) = apply {
+ clientOptions.removeAllQueryParams(keys)
+ }
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * @see ClientOptions.Builder.fromEnv
+ */
+ fun fromEnv() = apply { clientOptions.fromEnv() }
+
+ /**
+ * Returns an immutable instance of [ImageKitClientAsync].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): ImageKitClientAsync =
+ ImageKitClientAsyncImpl(
+ clientOptions
+ .httpClient(
+ OkHttpClient.builder()
+ .timeout(clientOptions.timeout())
+ .proxy(proxy)
+ .sslSocketFactory(sslSocketFactory)
+ .trustManager(trustManager)
+ .hostnameVerifier(hostnameVerifier)
+ .build()
+ )
+ .build()
+ )
+ }
+}
diff --git a/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/OkHttpClient.kt b/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/OkHttpClient.kt
new file mode 100644
index 0000000..f86881b
--- /dev/null
+++ b/image-kit-java-client-okhttp/src/main/kotlin/com/imagekit/api/client/okhttp/OkHttpClient.kt
@@ -0,0 +1,246 @@
+package com.imagekit.api.client.okhttp
+
+import com.imagekit.api.core.RequestOptions
+import com.imagekit.api.core.Timeout
+import com.imagekit.api.core.http.Headers
+import com.imagekit.api.core.http.HttpClient
+import com.imagekit.api.core.http.HttpMethod
+import com.imagekit.api.core.http.HttpRequest
+import com.imagekit.api.core.http.HttpRequestBody
+import com.imagekit.api.core.http.HttpResponse
+import com.imagekit.api.errors.ImageKitIoException
+import java.io.IOException
+import java.io.InputStream
+import java.net.Proxy
+import java.time.Duration
+import java.util.concurrent.CompletableFuture
+import javax.net.ssl.HostnameVerifier
+import javax.net.ssl.SSLSocketFactory
+import javax.net.ssl.X509TrustManager
+import okhttp3.Call
+import okhttp3.Callback
+import okhttp3.HttpUrl.Companion.toHttpUrl
+import okhttp3.MediaType
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.Request
+import okhttp3.RequestBody
+import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
+import okhttp3.logging.HttpLoggingInterceptor
+import okio.BufferedSink
+
+class OkHttpClient private constructor(private val okHttpClient: okhttp3.OkHttpClient) :
+ HttpClient {
+
+ override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse {
+ val call = newCall(request, requestOptions)
+
+ return try {
+ call.execute().toResponse()
+ } catch (e: IOException) {
+ throw ImageKitIoException("Request failed", e)
+ } finally {
+ request.body?.close()
+ }
+ }
+
+ override fun executeAsync(
+ request: HttpRequest,
+ requestOptions: RequestOptions,
+ ): CompletableFuture {
+ val future = CompletableFuture()
+
+ request.body?.run { future.whenComplete { _, _ -> close() } }
+
+ newCall(request, requestOptions)
+ .enqueue(
+ object : Callback {
+ override fun onResponse(call: Call, response: Response) {
+ future.complete(response.toResponse())
+ }
+
+ override fun onFailure(call: Call, e: IOException) {
+ future.completeExceptionally(ImageKitIoException("Request failed", e))
+ }
+ }
+ )
+
+ return future
+ }
+
+ override fun close() {
+ okHttpClient.dispatcher.executorService.shutdown()
+ okHttpClient.connectionPool.evictAll()
+ okHttpClient.cache?.close()
+ }
+
+ private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
+ val clientBuilder = okHttpClient.newBuilder()
+
+ val logLevel =
+ when (System.getenv("IMAGE_KIT_LOG")?.lowercase()) {
+ "info" -> HttpLoggingInterceptor.Level.BASIC
+ "debug" -> HttpLoggingInterceptor.Level.BODY
+ else -> null
+ }
+ if (logLevel != null) {
+ clientBuilder.addNetworkInterceptor(
+ HttpLoggingInterceptor().setLevel(logLevel).apply { redactHeader("Authorization") }
+ )
+ }
+
+ requestOptions.timeout?.let {
+ clientBuilder
+ .connectTimeout(it.connect())
+ .readTimeout(it.read())
+ .writeTimeout(it.write())
+ .callTimeout(it.request())
+ }
+
+ val client = clientBuilder.build()
+ return client.newCall(request.toRequest(client))
+ }
+
+ private fun HttpRequest.toRequest(client: okhttp3.OkHttpClient): Request {
+ var body: RequestBody? = body?.toRequestBody()
+ if (body == null && requiresBody(method)) {
+ body = "".toRequestBody()
+ }
+
+ val builder = Request.Builder().url(toUrl()).method(method.name, body)
+ headers.names().forEach { name ->
+ headers.values(name).forEach { builder.header(name, it) }
+ }
+
+ if (
+ !headers.names().contains("X-Stainless-Read-Timeout") && client.readTimeoutMillis != 0
+ ) {
+ builder.header(
+ "X-Stainless-Read-Timeout",
+ Duration.ofMillis(client.readTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+ if (!headers.names().contains("X-Stainless-Timeout") && client.callTimeoutMillis != 0) {
+ builder.header(
+ "X-Stainless-Timeout",
+ Duration.ofMillis(client.callTimeoutMillis.toLong()).seconds.toString(),
+ )
+ }
+
+ return builder.build()
+ }
+
+ /** `OkHttpClient` always requires a request body for some methods. */
+ private fun requiresBody(method: HttpMethod): Boolean =
+ when (method) {
+ HttpMethod.POST,
+ HttpMethod.PUT,
+ HttpMethod.PATCH -> true
+ else -> false
+ }
+
+ private fun HttpRequest.toUrl(): String {
+ val builder = baseUrl.toHttpUrl().newBuilder()
+ pathSegments.forEach(builder::addPathSegment)
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { builder.addQueryParameter(key, it) }
+ }
+
+ return builder.toString()
+ }
+
+ private fun HttpRequestBody.toRequestBody(): RequestBody {
+ val mediaType = contentType()?.toMediaType()
+ val length = contentLength()
+
+ return object : RequestBody() {
+ override fun contentType(): MediaType? = mediaType
+
+ override fun contentLength(): Long = length
+
+ override fun isOneShot(): Boolean = !repeatable()
+
+ override fun writeTo(sink: BufferedSink) = writeTo(sink.outputStream())
+ }
+ }
+
+ private fun Response.toResponse(): HttpResponse {
+ val headers = headers.toHeaders()
+
+ return object : HttpResponse {
+ override fun statusCode(): Int = code
+
+ override fun headers(): Headers = headers
+
+ override fun body(): InputStream = body!!.byteStream()
+
+ override fun close() = body!!.close()
+ }
+ }
+
+ private fun okhttp3.Headers.toHeaders(): Headers {
+ val headersBuilder = Headers.builder()
+ forEach { (name, value) -> headersBuilder.put(name, value) }
+ return headersBuilder.build()
+ }
+
+ companion object {
+ @JvmStatic fun builder() = Builder()
+ }
+
+ class Builder internal constructor() {
+
+ private var timeout: Timeout = Timeout.default()
+ private var proxy: Proxy? = null
+ private var sslSocketFactory: SSLSocketFactory? = null
+ private var trustManager: X509TrustManager? = null
+ private var hostnameVerifier: HostnameVerifier? = null
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun proxy(proxy: Proxy?) = apply { this.proxy = proxy }
+
+ fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply {
+ this.sslSocketFactory = sslSocketFactory
+ }
+
+ fun trustManager(trustManager: X509TrustManager?) = apply {
+ this.trustManager = trustManager
+ }
+
+ fun hostnameVerifier(hostnameVerifier: HostnameVerifier?) = apply {
+ this.hostnameVerifier = hostnameVerifier
+ }
+
+ fun build(): OkHttpClient =
+ OkHttpClient(
+ okhttp3.OkHttpClient.Builder()
+ .connectTimeout(timeout.connect())
+ .readTimeout(timeout.read())
+ .writeTimeout(timeout.write())
+ .callTimeout(timeout.request())
+ .proxy(proxy)
+ .apply {
+ val sslSocketFactory = sslSocketFactory
+ val trustManager = trustManager
+ if (sslSocketFactory != null && trustManager != null) {
+ sslSocketFactory(sslSocketFactory, trustManager)
+ } else {
+ check((sslSocketFactory != null) == (trustManager != null)) {
+ "Both or none of `sslSocketFactory` and `trustManager` must be set, but only one was set"
+ }
+ }
+
+ hostnameVerifier?.let(::hostnameVerifier)
+ }
+ .build()
+ .apply {
+ // We usually make all our requests to the same host so it makes sense to
+ // raise the per-host limit to the overall limit.
+ dispatcher.maxRequestsPerHost = dispatcher.maxRequests
+ }
+ )
+ }
+}
diff --git a/image-kit-java-core/build.gradle.kts b/image-kit-java-core/build.gradle.kts
new file mode 100644
index 0000000..9226395
--- /dev/null
+++ b/image-kit-java-core/build.gradle.kts
@@ -0,0 +1,41 @@
+plugins {
+ id("image-kit.kotlin")
+ id("image-kit.publish")
+}
+
+configurations.all {
+ resolutionStrategy {
+ // Compile and test against a lower Jackson version to ensure we're compatible with it.
+ // We publish with a higher version (see below) to ensure users depend on a secure version by default.
+ force("com.fasterxml.jackson.core:jackson-core:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-databind:2.13.4")
+ force("com.fasterxml.jackson.core:jackson-annotations:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4")
+ force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4")
+ force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4")
+ }
+}
+
+dependencies {
+ api("com.fasterxml.jackson.core:jackson-core:2.18.2")
+ api("com.fasterxml.jackson.core:jackson-databind:2.18.2")
+ api("com.google.errorprone:error_prone_annotations:2.33.0")
+
+ implementation("com.fasterxml.jackson.core:jackson-annotations:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.18.2")
+ implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.18.2")
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.18.2")
+ implementation("org.apache.httpcomponents.core5:httpcore5:5.2.4")
+ implementation("org.apache.httpcomponents.client5:httpclient5:5.3.1")
+
+ testImplementation(kotlin("test"))
+ testImplementation(project(":image-kit-java-client-okhttp"))
+ testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.2")
+ testImplementation("org.assertj:assertj-core:3.25.3")
+ testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3")
+ testImplementation("org.junit.jupiter:junit-jupiter-params:5.9.3")
+ testImplementation("org.junit-pioneer:junit-pioneer:1.9.1")
+ testImplementation("org.mockito:mockito-core:5.14.2")
+ testImplementation("org.mockito:mockito-junit-jupiter:5.14.2")
+ testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0")
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClient.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClient.kt
new file mode 100644
index 0000000..ff5e227
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClient.kt
@@ -0,0 +1,107 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.client
+
+import com.imagekit.api.core.ClientOptions
+import com.imagekit.api.services.blocking.AccountService
+import com.imagekit.api.services.blocking.AssetService
+import com.imagekit.api.services.blocking.BetaService
+import com.imagekit.api.services.blocking.CacheService
+import com.imagekit.api.services.blocking.CustomMetadataFieldService
+import com.imagekit.api.services.blocking.FileService
+import com.imagekit.api.services.blocking.FolderService
+import com.imagekit.api.services.blocking.WebhookService
+import java.util.function.Consumer
+
+/**
+ * A client for interacting with the Image Kit REST API synchronously. You can also switch to
+ * asynchronous execution via the [async] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
+interface ImageKitClient {
+
+ /**
+ * Returns a version of this client that uses asynchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
+ fun async(): ImageKitClientAsync
+
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): ImageKitClient
+
+ fun customMetadataFields(): CustomMetadataFieldService
+
+ fun files(): FileService
+
+ fun assets(): AssetService
+
+ fun cache(): CacheService
+
+ fun folders(): FolderService
+
+ fun accounts(): AccountService
+
+ fun beta(): BetaService
+
+ fun webhooks(): WebhookService
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /** A view of [ImageKitClient] that provides access to raw HTTP responses for each method. */
+ interface WithRawResponse {
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): ImageKitClient.WithRawResponse
+
+ fun customMetadataFields(): CustomMetadataFieldService.WithRawResponse
+
+ fun files(): FileService.WithRawResponse
+
+ fun assets(): AssetService.WithRawResponse
+
+ fun cache(): CacheService.WithRawResponse
+
+ fun folders(): FolderService.WithRawResponse
+
+ fun accounts(): AccountService.WithRawResponse
+
+ fun beta(): BetaService.WithRawResponse
+
+ fun webhooks(): WebhookService.WithRawResponse
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsync.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsync.kt
new file mode 100644
index 0000000..2213152
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsync.kt
@@ -0,0 +1,111 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.client
+
+import com.imagekit.api.core.ClientOptions
+import com.imagekit.api.services.async.AccountServiceAsync
+import com.imagekit.api.services.async.AssetServiceAsync
+import com.imagekit.api.services.async.BetaServiceAsync
+import com.imagekit.api.services.async.CacheServiceAsync
+import com.imagekit.api.services.async.CustomMetadataFieldServiceAsync
+import com.imagekit.api.services.async.FileServiceAsync
+import com.imagekit.api.services.async.FolderServiceAsync
+import com.imagekit.api.services.async.WebhookServiceAsync
+import java.util.function.Consumer
+
+/**
+ * A client for interacting with the Image Kit REST API asynchronously. You can also switch to
+ * synchronous execution via the [sync] method.
+ *
+ * This client performs best when you create a single instance and reuse it for all interactions
+ * with the REST API. This is because each client holds its own connection pool and thread pools.
+ * Reusing connections and threads reduces latency and saves memory. The client also handles rate
+ * limiting per client. This means that creating and using multiple instances at the same time will
+ * not respect rate limits.
+ *
+ * The threads and connections that are held will be released automatically if they remain idle. But
+ * if you are writing an application that needs to aggressively release unused resources, then you
+ * may call [close].
+ */
+interface ImageKitClientAsync {
+
+ /**
+ * Returns a version of this client that uses synchronous execution.
+ *
+ * The returned client shares its resources, like its connection pool and thread pools, with
+ * this client.
+ */
+ fun sync(): ImageKitClient
+
+ /**
+ * Returns a view of this service that provides access to raw HTTP responses for each method.
+ */
+ fun withRawResponse(): WithRawResponse
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(modifier: Consumer): ImageKitClientAsync
+
+ fun customMetadataFields(): CustomMetadataFieldServiceAsync
+
+ fun files(): FileServiceAsync
+
+ fun assets(): AssetServiceAsync
+
+ fun cache(): CacheServiceAsync
+
+ fun folders(): FolderServiceAsync
+
+ fun accounts(): AccountServiceAsync
+
+ fun beta(): BetaServiceAsync
+
+ fun webhooks(): WebhookServiceAsync
+
+ /**
+ * Closes this client, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client is long-lived and
+ * usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default HTTP client
+ * automatically releases threads and connections if they remain idle, but if you are writing an
+ * application that needs to aggressively release unused resources, then you may call this
+ * method.
+ */
+ fun close()
+
+ /**
+ * A view of [ImageKitClientAsync] that provides access to raw HTTP responses for each method.
+ */
+ interface WithRawResponse {
+
+ /**
+ * Returns a view of this service with the given option modifications applied.
+ *
+ * The original service is not modified.
+ */
+ fun withOptions(
+ modifier: Consumer
+ ): ImageKitClientAsync.WithRawResponse
+
+ fun customMetadataFields(): CustomMetadataFieldServiceAsync.WithRawResponse
+
+ fun files(): FileServiceAsync.WithRawResponse
+
+ fun assets(): AssetServiceAsync.WithRawResponse
+
+ fun cache(): CacheServiceAsync.WithRawResponse
+
+ fun folders(): FolderServiceAsync.WithRawResponse
+
+ fun accounts(): AccountServiceAsync.WithRawResponse
+
+ fun beta(): BetaServiceAsync.WithRawResponse
+
+ fun webhooks(): WebhookServiceAsync.WithRawResponse
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt
new file mode 100644
index 0000000..4ddc059
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientAsyncImpl.kt
@@ -0,0 +1,154 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.client
+
+import com.imagekit.api.core.ClientOptions
+import com.imagekit.api.core.getPackageVersion
+import com.imagekit.api.services.async.AccountServiceAsync
+import com.imagekit.api.services.async.AccountServiceAsyncImpl
+import com.imagekit.api.services.async.AssetServiceAsync
+import com.imagekit.api.services.async.AssetServiceAsyncImpl
+import com.imagekit.api.services.async.BetaServiceAsync
+import com.imagekit.api.services.async.BetaServiceAsyncImpl
+import com.imagekit.api.services.async.CacheServiceAsync
+import com.imagekit.api.services.async.CacheServiceAsyncImpl
+import com.imagekit.api.services.async.CustomMetadataFieldServiceAsync
+import com.imagekit.api.services.async.CustomMetadataFieldServiceAsyncImpl
+import com.imagekit.api.services.async.FileServiceAsync
+import com.imagekit.api.services.async.FileServiceAsyncImpl
+import com.imagekit.api.services.async.FolderServiceAsync
+import com.imagekit.api.services.async.FolderServiceAsyncImpl
+import com.imagekit.api.services.async.WebhookServiceAsync
+import com.imagekit.api.services.async.WebhookServiceAsyncImpl
+import java.util.function.Consumer
+
+class ImageKitClientAsyncImpl(private val clientOptions: ClientOptions) : ImageKitClientAsync {
+
+ private val clientOptionsWithUserAgent =
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
+ else
+ clientOptions
+ .toBuilder()
+ .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
+ .build()
+
+ // Pass the original clientOptions so that this client sets its own User-Agent.
+ private val sync: ImageKitClient by lazy { ImageKitClientImpl(clientOptions) }
+
+ private val withRawResponse: ImageKitClientAsync.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val customMetadataFields: CustomMetadataFieldServiceAsync by lazy {
+ CustomMetadataFieldServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val files: FileServiceAsync by lazy { FileServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val assets: AssetServiceAsync by lazy {
+ AssetServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val cache: CacheServiceAsync by lazy {
+ CacheServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val folders: FolderServiceAsync by lazy {
+ FolderServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val accounts: AccountServiceAsync by lazy {
+ AccountServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ private val beta: BetaServiceAsync by lazy { BetaServiceAsyncImpl(clientOptionsWithUserAgent) }
+
+ private val webhooks: WebhookServiceAsync by lazy {
+ WebhookServiceAsyncImpl(clientOptionsWithUserAgent)
+ }
+
+ override fun sync(): ImageKitClient = sync
+
+ override fun withRawResponse(): ImageKitClientAsync.WithRawResponse = withRawResponse
+
+ override fun withOptions(modifier: Consumer): ImageKitClientAsync =
+ ImageKitClientAsyncImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
+ override fun customMetadataFields(): CustomMetadataFieldServiceAsync = customMetadataFields
+
+ override fun files(): FileServiceAsync = files
+
+ override fun assets(): AssetServiceAsync = assets
+
+ override fun cache(): CacheServiceAsync = cache
+
+ override fun folders(): FolderServiceAsync = folders
+
+ override fun accounts(): AccountServiceAsync = accounts
+
+ override fun beta(): BetaServiceAsync = beta
+
+ override fun webhooks(): WebhookServiceAsync = webhooks
+
+ override fun close() = clientOptions.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ ImageKitClientAsync.WithRawResponse {
+
+ private val customMetadataFields: CustomMetadataFieldServiceAsync.WithRawResponse by lazy {
+ CustomMetadataFieldServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val files: FileServiceAsync.WithRawResponse by lazy {
+ FileServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val assets: AssetServiceAsync.WithRawResponse by lazy {
+ AssetServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val cache: CacheServiceAsync.WithRawResponse by lazy {
+ CacheServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val folders: FolderServiceAsync.WithRawResponse by lazy {
+ FolderServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val accounts: AccountServiceAsync.WithRawResponse by lazy {
+ AccountServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val beta: BetaServiceAsync.WithRawResponse by lazy {
+ BetaServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val webhooks: WebhookServiceAsync.WithRawResponse by lazy {
+ WebhookServiceAsyncImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun withOptions(
+ modifier: Consumer
+ ): ImageKitClientAsync.WithRawResponse =
+ ImageKitClientAsyncImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
+ override fun customMetadataFields(): CustomMetadataFieldServiceAsync.WithRawResponse =
+ customMetadataFields
+
+ override fun files(): FileServiceAsync.WithRawResponse = files
+
+ override fun assets(): AssetServiceAsync.WithRawResponse = assets
+
+ override fun cache(): CacheServiceAsync.WithRawResponse = cache
+
+ override fun folders(): FolderServiceAsync.WithRawResponse = folders
+
+ override fun accounts(): AccountServiceAsync.WithRawResponse = accounts
+
+ override fun beta(): BetaServiceAsync.WithRawResponse = beta
+
+ override fun webhooks(): WebhookServiceAsync.WithRawResponse = webhooks
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt
new file mode 100644
index 0000000..433b100
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/client/ImageKitClientImpl.kt
@@ -0,0 +1,144 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.client
+
+import com.imagekit.api.core.ClientOptions
+import com.imagekit.api.core.getPackageVersion
+import com.imagekit.api.services.blocking.AccountService
+import com.imagekit.api.services.blocking.AccountServiceImpl
+import com.imagekit.api.services.blocking.AssetService
+import com.imagekit.api.services.blocking.AssetServiceImpl
+import com.imagekit.api.services.blocking.BetaService
+import com.imagekit.api.services.blocking.BetaServiceImpl
+import com.imagekit.api.services.blocking.CacheService
+import com.imagekit.api.services.blocking.CacheServiceImpl
+import com.imagekit.api.services.blocking.CustomMetadataFieldService
+import com.imagekit.api.services.blocking.CustomMetadataFieldServiceImpl
+import com.imagekit.api.services.blocking.FileService
+import com.imagekit.api.services.blocking.FileServiceImpl
+import com.imagekit.api.services.blocking.FolderService
+import com.imagekit.api.services.blocking.FolderServiceImpl
+import com.imagekit.api.services.blocking.WebhookService
+import com.imagekit.api.services.blocking.WebhookServiceImpl
+import java.util.function.Consumer
+
+class ImageKitClientImpl(private val clientOptions: ClientOptions) : ImageKitClient {
+
+ private val clientOptionsWithUserAgent =
+ if (clientOptions.headers.names().contains("User-Agent")) clientOptions
+ else
+ clientOptions
+ .toBuilder()
+ .putHeader("User-Agent", "${javaClass.simpleName}/Java ${getPackageVersion()}")
+ .build()
+
+ // Pass the original clientOptions so that this client sets its own User-Agent.
+ private val async: ImageKitClientAsync by lazy { ImageKitClientAsyncImpl(clientOptions) }
+
+ private val withRawResponse: ImageKitClient.WithRawResponse by lazy {
+ WithRawResponseImpl(clientOptions)
+ }
+
+ private val customMetadataFields: CustomMetadataFieldService by lazy {
+ CustomMetadataFieldServiceImpl(clientOptionsWithUserAgent)
+ }
+
+ private val files: FileService by lazy { FileServiceImpl(clientOptionsWithUserAgent) }
+
+ private val assets: AssetService by lazy { AssetServiceImpl(clientOptionsWithUserAgent) }
+
+ private val cache: CacheService by lazy { CacheServiceImpl(clientOptionsWithUserAgent) }
+
+ private val folders: FolderService by lazy { FolderServiceImpl(clientOptionsWithUserAgent) }
+
+ private val accounts: AccountService by lazy { AccountServiceImpl(clientOptionsWithUserAgent) }
+
+ private val beta: BetaService by lazy { BetaServiceImpl(clientOptionsWithUserAgent) }
+
+ private val webhooks: WebhookService by lazy { WebhookServiceImpl(clientOptionsWithUserAgent) }
+
+ override fun async(): ImageKitClientAsync = async
+
+ override fun withRawResponse(): ImageKitClient.WithRawResponse = withRawResponse
+
+ override fun withOptions(modifier: Consumer): ImageKitClient =
+ ImageKitClientImpl(clientOptions.toBuilder().apply(modifier::accept).build())
+
+ override fun customMetadataFields(): CustomMetadataFieldService = customMetadataFields
+
+ override fun files(): FileService = files
+
+ override fun assets(): AssetService = assets
+
+ override fun cache(): CacheService = cache
+
+ override fun folders(): FolderService = folders
+
+ override fun accounts(): AccountService = accounts
+
+ override fun beta(): BetaService = beta
+
+ override fun webhooks(): WebhookService = webhooks
+
+ override fun close() = clientOptions.close()
+
+ class WithRawResponseImpl internal constructor(private val clientOptions: ClientOptions) :
+ ImageKitClient.WithRawResponse {
+
+ private val customMetadataFields: CustomMetadataFieldService.WithRawResponse by lazy {
+ CustomMetadataFieldServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val files: FileService.WithRawResponse by lazy {
+ FileServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val assets: AssetService.WithRawResponse by lazy {
+ AssetServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val cache: CacheService.WithRawResponse by lazy {
+ CacheServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val folders: FolderService.WithRawResponse by lazy {
+ FolderServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val accounts: AccountService.WithRawResponse by lazy {
+ AccountServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val beta: BetaService.WithRawResponse by lazy {
+ BetaServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ private val webhooks: WebhookService.WithRawResponse by lazy {
+ WebhookServiceImpl.WithRawResponseImpl(clientOptions)
+ }
+
+ override fun withOptions(
+ modifier: Consumer
+ ): ImageKitClient.WithRawResponse =
+ ImageKitClientImpl.WithRawResponseImpl(
+ clientOptions.toBuilder().apply(modifier::accept).build()
+ )
+
+ override fun customMetadataFields(): CustomMetadataFieldService.WithRawResponse =
+ customMetadataFields
+
+ override fun files(): FileService.WithRawResponse = files
+
+ override fun assets(): AssetService.WithRawResponse = assets
+
+ override fun cache(): CacheService.WithRawResponse = cache
+
+ override fun folders(): FolderService.WithRawResponse = folders
+
+ override fun accounts(): AccountService.WithRawResponse = accounts
+
+ override fun beta(): BetaService.WithRawResponse = beta
+
+ override fun webhooks(): WebhookService.WithRawResponse = webhooks
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/BaseDeserializer.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/BaseDeserializer.kt
new file mode 100644
index 0000000..106fb74
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/BaseDeserializer.kt
@@ -0,0 +1,44 @@
+package com.imagekit.api.core
+
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.core.ObjectCodec
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.BeanProperty
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JavaType
+import com.fasterxml.jackson.databind.JsonDeserializer
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.deser.ContextualDeserializer
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import kotlin.reflect.KClass
+
+abstract class BaseDeserializer(type: KClass) :
+ StdDeserializer(type.java), ContextualDeserializer {
+
+ override fun createContextual(
+ context: DeserializationContext,
+ property: BeanProperty?,
+ ): JsonDeserializer {
+ return this
+ }
+
+ override fun deserialize(parser: JsonParser, context: DeserializationContext): T {
+ return parser.codec.deserialize(parser.readValueAsTree())
+ }
+
+ protected abstract fun ObjectCodec.deserialize(node: JsonNode): T
+
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: TypeReference): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
+ null
+ }
+
+ protected fun ObjectCodec.tryDeserialize(node: JsonNode, type: JavaType): T? =
+ try {
+ readValue(treeAsTokens(node), type)
+ } catch (e: Exception) {
+ null
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/BaseSerializer.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/BaseSerializer.kt
new file mode 100644
index 0000000..b0d195b
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/BaseSerializer.kt
@@ -0,0 +1,6 @@
+package com.imagekit.api.core
+
+import com.fasterxml.jackson.databind.ser.std.StdSerializer
+import kotlin.reflect.KClass
+
+abstract class BaseSerializer(type: KClass) : StdSerializer(type.java)
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Check.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Check.kt
new file mode 100644
index 0000000..f587fcc
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Check.kt
@@ -0,0 +1,96 @@
+@file:JvmName("Check")
+
+package com.imagekit.api.core
+
+import com.fasterxml.jackson.core.Version
+import com.fasterxml.jackson.core.util.VersionUtil
+
+fun checkRequired(name: String, condition: Boolean) =
+ check(condition) { "`$name` is required, but was not set" }
+
+fun checkRequired(name: String, value: T?): T =
+ checkNotNull(value) { "`$name` is required, but was not set" }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: JsonField): T =
+ value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkKnown(name: String, value: MultipartField): T =
+ value.value.asKnown().orElseThrow {
+ IllegalStateException("`$name` is not a known type: ${value.javaClass.simpleName}")
+ }
+
+@JvmSynthetic
+internal fun checkLength(name: String, value: String, length: Int): String =
+ value.also {
+ check(it.length == length) { "`$name` must have length $length, but was ${it.length}" }
+ }
+
+@JvmSynthetic
+internal fun checkMinLength(name: String, value: String, minLength: Int): String =
+ value.also {
+ check(it.length >= minLength) {
+ if (minLength == 1) "`$name` must be non-empty, but was empty"
+ else "`$name` must have at least length $minLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkMaxLength(name: String, value: String, maxLength: Int): String =
+ value.also {
+ check(it.length <= maxLength) {
+ "`$name` must have at most length $maxLength, but was ${it.length}"
+ }
+ }
+
+@JvmSynthetic
+internal fun checkJacksonVersionCompatibility() {
+ val incompatibleJacksonVersions =
+ RUNTIME_JACKSON_VERSIONS.mapNotNull {
+ val badVersionReason = BAD_JACKSON_VERSIONS[it.toString()]
+ when {
+ it.majorVersion != MINIMUM_JACKSON_VERSION.majorVersion ->
+ it to "incompatible major version"
+ it.minorVersion < MINIMUM_JACKSON_VERSION.minorVersion ->
+ it to "minor version too low"
+ it.minorVersion == MINIMUM_JACKSON_VERSION.minorVersion &&
+ it.patchLevel < MINIMUM_JACKSON_VERSION.patchLevel ->
+ it to "patch version too low"
+ badVersionReason != null -> it to badVersionReason
+ else -> null
+ }
+ }
+ check(incompatibleJacksonVersions.isEmpty()) {
+ """
+This SDK requires a minimum Jackson version of $MINIMUM_JACKSON_VERSION, but the following incompatible Jackson versions were detected at runtime:
+
+${incompatibleJacksonVersions.asSequence().map { (version, incompatibilityReason) ->
+ "- `${version.toFullString().replace("/", ":")}` ($incompatibilityReason)"
+}.joinToString("\n")}
+
+This can happen if you are either:
+1. Directly depending on different Jackson versions
+2. Depending on some library that depends on different Jackson versions, potentially transitively
+
+Double-check that you are depending on compatible Jackson versions.
+
+See https://www.github.com/imagekit-developer/imagekit-java#jackson for more information.
+ """
+ .trimIndent()
+ }
+}
+
+private val MINIMUM_JACKSON_VERSION: Version = VersionUtil.parseVersion("2.13.4", null, null)
+private val BAD_JACKSON_VERSIONS: Map =
+ mapOf("2.18.1" to "due to https://github.com/FasterXML/jackson-databind/issues/4639")
+private val RUNTIME_JACKSON_VERSIONS: List =
+ listOf(
+ com.fasterxml.jackson.core.json.PackageVersion.VERSION,
+ com.fasterxml.jackson.databind.cfg.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jdk8.PackageVersion.VERSION,
+ com.fasterxml.jackson.datatype.jsr310.PackageVersion.VERSION,
+ com.fasterxml.jackson.module.kotlin.PackageVersion.VERSION,
+ )
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/ClientOptions.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/ClientOptions.kt
new file mode 100644
index 0000000..7b379cd
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/ClientOptions.kt
@@ -0,0 +1,496 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.core
+
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.imagekit.api.core.http.Headers
+import com.imagekit.api.core.http.HttpClient
+import com.imagekit.api.core.http.PhantomReachableClosingHttpClient
+import com.imagekit.api.core.http.QueryParams
+import com.imagekit.api.core.http.RetryingHttpClient
+import java.time.Clock
+import java.time.Duration
+import java.util.Base64
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class representing the SDK client configuration. */
+class ClientOptions
+private constructor(
+ private val originalHttpClient: HttpClient,
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `image-kit-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
+ @get:JvmName("httpClient") val httpClient: HttpClient,
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee that
+ * the SDK will work correctly when using an incompatible Jackson version.
+ */
+ @get:JvmName("checkJacksonVersionCompatibility") val checkJacksonVersionCompatibility: Boolean,
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.imagekit.api.core.jsonMapper]. The default is usually sufficient and rarely
+ * needs to be overridden.
+ */
+ @get:JvmName("jsonMapper") val jsonMapper: JsonMapper,
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ @get:JvmName("clock") val clock: Clock,
+ private val baseUrl: String?,
+ /** Headers to send with the request. */
+ @get:JvmName("headers") val headers: Headers,
+ /** Query params to send with the request. */
+ @get:JvmName("queryParams") val queryParams: QueryParams,
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ @get:JvmName("responseValidation") val responseValidation: Boolean,
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ @get:JvmName("timeout") val timeout: Timeout,
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
+ @get:JvmName("maxRetries") val maxRetries: Int,
+ /**
+ * Your ImageKit private API key (it starts with `private_`). You can view and manage API keys
+ * in the [dashboard](https://imagekit.io/dashboard/developer/api-keys).
+ */
+ @get:JvmName("privateApiKey") val privateApiKey: String,
+ private val password: String?,
+ private val webhookSecret: String?,
+) {
+
+ init {
+ if (checkJacksonVersionCompatibility) {
+ checkJacksonVersionCompatibility()
+ }
+ }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.imagekit.io`.
+ */
+ fun baseUrl(): String = baseUrl ?: PRODUCTION_URL
+
+ fun baseUrlOverridden(): Boolean = baseUrl != null
+
+ /**
+ * ImageKit Basic Auth only uses the username field and ignores the password. This field is
+ * unused.
+ *
+ * Defaults to `"do_not_set"`.
+ */
+ fun password(): Optional = Optional.ofNullable(password)
+
+ /**
+ * Your ImageKit webhook secret. This is used by the SDK to verify webhook signatures. It starts
+ * with a `whsec_` prefix. You can view and manage your webhook secret in the
+ * [dashboard](https://imagekit.io/dashboard/developer/webhooks). Treat the secret like a
+ * password, keep it private and do not expose it publicly. Learn more about
+ * [webhook verification](https://imagekit.io/docs/webhooks#verify-webhook-signature).
+ */
+ fun webhookSecret(): Optional = Optional.ofNullable(webhookSecret)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ const val PRODUCTION_URL = "https://api.imagekit.io"
+
+ /**
+ * Returns a mutable builder for constructing an instance of [ClientOptions].
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .privateApiKey()
+ * ```
+ */
+ @JvmStatic fun builder() = Builder()
+
+ /**
+ * Returns options configured using system properties and environment variables.
+ *
+ * @see Builder.fromEnv
+ */
+ @JvmStatic fun fromEnv(): ClientOptions = builder().fromEnv().build()
+ }
+
+ /** A builder for [ClientOptions]. */
+ class Builder internal constructor() {
+
+ private var httpClient: HttpClient? = null
+ private var checkJacksonVersionCompatibility: Boolean = true
+ private var jsonMapper: JsonMapper = jsonMapper()
+ private var clock: Clock = Clock.systemUTC()
+ private var baseUrl: String? = null
+ private var headers: Headers.Builder = Headers.builder()
+ private var queryParams: QueryParams.Builder = QueryParams.builder()
+ private var responseValidation: Boolean = false
+ private var timeout: Timeout = Timeout.default()
+ private var maxRetries: Int = 2
+ private var privateApiKey: String? = null
+ private var password: String? = "do_not_set"
+ private var webhookSecret: String? = null
+
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions) = apply {
+ httpClient = clientOptions.originalHttpClient
+ checkJacksonVersionCompatibility = clientOptions.checkJacksonVersionCompatibility
+ jsonMapper = clientOptions.jsonMapper
+ clock = clientOptions.clock
+ baseUrl = clientOptions.baseUrl
+ headers = clientOptions.headers.toBuilder()
+ queryParams = clientOptions.queryParams.toBuilder()
+ responseValidation = clientOptions.responseValidation
+ timeout = clientOptions.timeout
+ maxRetries = clientOptions.maxRetries
+ privateApiKey = clientOptions.privateApiKey
+ password = clientOptions.password
+ webhookSecret = clientOptions.webhookSecret
+ }
+
+ /**
+ * The HTTP client to use in the SDK.
+ *
+ * Use the one published in `image-kit-java-client-okhttp` or implement your own.
+ *
+ * This class takes ownership of the client and closes it when closed.
+ */
+ fun httpClient(httpClient: HttpClient) = apply {
+ this.httpClient = PhantomReachableClosingHttpClient(httpClient)
+ }
+
+ /**
+ * Whether to throw an exception if any of the Jackson versions detected at runtime are
+ * incompatible with the SDK's minimum supported Jackson version (2.13.4).
+ *
+ * Defaults to true. Use extreme caution when disabling this option. There is no guarantee
+ * that the SDK will work correctly when using an incompatible Jackson version.
+ */
+ fun checkJacksonVersionCompatibility(checkJacksonVersionCompatibility: Boolean) = apply {
+ this.checkJacksonVersionCompatibility = checkJacksonVersionCompatibility
+ }
+
+ /**
+ * The Jackson JSON mapper to use for serializing and deserializing JSON.
+ *
+ * Defaults to [com.imagekit.api.core.jsonMapper]. The default is usually sufficient and
+ * rarely needs to be overridden.
+ */
+ fun jsonMapper(jsonMapper: JsonMapper) = apply { this.jsonMapper = jsonMapper }
+
+ /**
+ * The clock to use for operations that require timing, like retries.
+ *
+ * This is primarily useful for using a fake clock in tests.
+ *
+ * Defaults to [Clock.systemUTC].
+ */
+ fun clock(clock: Clock) = apply { this.clock = clock }
+
+ /**
+ * The base URL to use for every request.
+ *
+ * Defaults to the production environment: `https://api.imagekit.io`.
+ */
+ fun baseUrl(baseUrl: String?) = apply { this.baseUrl = baseUrl }
+
+ /** Alias for calling [Builder.baseUrl] with `baseUrl.orElse(null)`. */
+ fun baseUrl(baseUrl: Optional) = baseUrl(baseUrl.getOrNull())
+
+ /**
+ * Whether to call `validate` on every response before returning it.
+ *
+ * Defaults to false, which means the shape of the response will not be validated upfront.
+ * Instead, validation will only occur for the parts of the response that are accessed.
+ */
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ /**
+ * Sets the maximum time allowed for various parts of an HTTP call's lifecycle, excluding
+ * retries.
+ *
+ * Defaults to [Timeout.default].
+ */
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ /**
+ * Sets the maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * See [Timeout.request] for more details.
+ *
+ * For fine-grained control, pass a [Timeout] object.
+ */
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ /**
+ * The maximum number of times to retry failed requests, with a short exponential backoff
+ * between requests.
+ *
+ * Only the following error types are retried:
+ * - Connection errors (for example, due to a network connectivity problem)
+ * - 408 Request Timeout
+ * - 409 Conflict
+ * - 429 Rate Limit
+ * - 5xx Internal
+ *
+ * The API may also explicitly instruct the SDK to retry or not retry a request.
+ *
+ * Defaults to 2.
+ */
+ fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
+
+ /**
+ * Your ImageKit private API key (it starts with `private_`). You can view and manage API
+ * keys in the [dashboard](https://imagekit.io/dashboard/developer/api-keys).
+ */
+ fun privateApiKey(privateApiKey: String) = apply { this.privateApiKey = privateApiKey }
+
+ /**
+ * ImageKit Basic Auth only uses the username field and ignores the password. This field is
+ * unused.
+ *
+ * Defaults to `"do_not_set"`.
+ */
+ fun password(password: String?) = apply { this.password = password }
+
+ /** Alias for calling [Builder.password] with `password.orElse(null)`. */
+ fun password(password: Optional) = password(password.getOrNull())
+
+ /**
+ * Your ImageKit webhook secret. This is used by the SDK to verify webhook signatures. It
+ * starts with a `whsec_` prefix. You can view and manage your webhook secret in the
+ * [dashboard](https://imagekit.io/dashboard/developer/webhooks). Treat the secret like a
+ * password, keep it private and do not expose it publicly. Learn more about
+ * [webhook verification](https://imagekit.io/docs/webhooks#verify-webhook-signature).
+ */
+ fun webhookSecret(webhookSecret: String?) = apply { this.webhookSecret = webhookSecret }
+
+ /** Alias for calling [Builder.webhookSecret] with `webhookSecret.orElse(null)`. */
+ fun webhookSecret(webhookSecret: Optional) =
+ webhookSecret(webhookSecret.getOrNull())
+
+ fun headers(headers: Headers) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
+ fun headers(headers: Map>) = apply {
+ this.headers.clear()
+ putAllHeaders(headers)
+ }
+
+ fun putHeader(name: String, value: String) = apply { headers.put(name, value) }
+
+ fun putHeaders(name: String, values: Iterable) = apply { headers.put(name, values) }
+
+ fun putAllHeaders(headers: Headers) = apply { this.headers.putAll(headers) }
+
+ fun putAllHeaders(headers: Map>) = apply {
+ this.headers.putAll(headers)
+ }
+
+ fun replaceHeaders(name: String, value: String) = apply { headers.replace(name, value) }
+
+ fun replaceHeaders(name: String, values: Iterable) = apply {
+ headers.replace(name, values)
+ }
+
+ fun replaceAllHeaders(headers: Headers) = apply { this.headers.replaceAll(headers) }
+
+ fun replaceAllHeaders(headers: Map>) = apply {
+ this.headers.replaceAll(headers)
+ }
+
+ fun removeHeaders(name: String) = apply { headers.remove(name) }
+
+ fun removeAllHeaders(names: Set) = apply { headers.removeAll(names) }
+
+ fun queryParams(queryParams: QueryParams) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
+
+ fun queryParams(queryParams: Map>) = apply {
+ this.queryParams.clear()
+ putAllQueryParams(queryParams)
+ }
+
+ fun putQueryParam(key: String, value: String) = apply { queryParams.put(key, value) }
+
+ fun putQueryParams(key: String, values: Iterable) = apply {
+ queryParams.put(key, values)
+ }
+
+ fun putAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.putAll(queryParams)
+ }
+
+ fun putAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.putAll(queryParams)
+ }
+
+ fun replaceQueryParams(key: String, value: String) = apply {
+ queryParams.replace(key, value)
+ }
+
+ fun replaceQueryParams(key: String, values: Iterable) = apply {
+ queryParams.replace(key, values)
+ }
+
+ fun replaceAllQueryParams(queryParams: QueryParams) = apply {
+ this.queryParams.replaceAll(queryParams)
+ }
+
+ fun replaceAllQueryParams(queryParams: Map>) = apply {
+ this.queryParams.replaceAll(queryParams)
+ }
+
+ fun removeQueryParams(key: String) = apply { queryParams.remove(key) }
+
+ fun removeAllQueryParams(keys: Set) = apply { queryParams.removeAll(keys) }
+
+ fun timeout(): Timeout = timeout
+
+ /**
+ * Updates configuration using system properties and environment variables.
+ *
+ * See this table for the available options:
+ *
+ * |Setter |System property |Environment variable |Required|Default value |
+ * |---------------|--------------------------------------|--------------------------------|--------|---------------------------|
+ * |`privateApiKey`|`imagekit.imagekitPrivateApiKey` |`IMAGEKIT_PRIVATE_API_KEY` |true |- |
+ * |`password` |`imagekit.optionalImagekitIgnoresThis`|`OPTIONAL_IMAGEKIT_IGNORES_THIS`|false |`"do_not_set"` |
+ * |`webhookSecret`|`imagekit.imagekitWebhookSecret` |`IMAGEKIT_WEBHOOK_SECRET` |false |- |
+ * |`baseUrl` |`imagekit.baseUrl` |`IMAGE_KIT_BASE_URL` |true |`"https://api.imagekit.io"`|
+ *
+ * System properties take precedence over environment variables.
+ */
+ fun fromEnv() = apply {
+ (System.getProperty("imagekit.baseUrl") ?: System.getenv("IMAGE_KIT_BASE_URL"))?.let {
+ baseUrl(it)
+ }
+ (System.getProperty("imagekit.imagekitPrivateApiKey")
+ ?: System.getenv("IMAGEKIT_PRIVATE_API_KEY"))
+ ?.let { privateApiKey(it) }
+ (System.getProperty("imagekit.optionalImagekitIgnoresThis")
+ ?: System.getenv("OPTIONAL_IMAGEKIT_IGNORES_THIS"))
+ ?.let { password(it) }
+ (System.getProperty("imagekit.imagekitWebhookSecret")
+ ?: System.getenv("IMAGEKIT_WEBHOOK_SECRET"))
+ ?.let { webhookSecret(it) }
+ }
+
+ /**
+ * Returns an immutable instance of [ClientOptions].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .httpClient()
+ * .privateApiKey()
+ * ```
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): ClientOptions {
+ val httpClient = checkRequired("httpClient", httpClient)
+ val privateApiKey = checkRequired("privateApiKey", privateApiKey)
+
+ val headers = Headers.builder()
+ val queryParams = QueryParams.builder()
+ headers.put("X-Stainless-Lang", "java")
+ headers.put("X-Stainless-Arch", getOsArch())
+ headers.put("X-Stainless-OS", getOsName())
+ headers.put("X-Stainless-OS-Version", getOsVersion())
+ headers.put("X-Stainless-Package-Version", getPackageVersion())
+ headers.put("X-Stainless-Runtime", "JRE")
+ headers.put("X-Stainless-Runtime-Version", getJavaVersion())
+ privateApiKey.let { username ->
+ password?.let { password ->
+ if (!username.isEmpty() && !password.isEmpty()) {
+ headers.put(
+ "Authorization",
+ "Basic ${Base64.getEncoder().encodeToString("$username:$password".toByteArray())}",
+ )
+ }
+ }
+ }
+ headers.replaceAll(this.headers.build())
+ queryParams.replaceAll(this.queryParams.build())
+
+ return ClientOptions(
+ httpClient,
+ RetryingHttpClient.builder()
+ .httpClient(httpClient)
+ .clock(clock)
+ .maxRetries(maxRetries)
+ .build(),
+ checkJacksonVersionCompatibility,
+ jsonMapper,
+ clock,
+ baseUrl,
+ headers.build(),
+ queryParams.build(),
+ responseValidation,
+ timeout,
+ maxRetries,
+ privateApiKey,
+ password,
+ webhookSecret,
+ )
+ }
+ }
+
+ /**
+ * Closes these client options, relinquishing any underlying resources.
+ *
+ * This is purposefully not inherited from [AutoCloseable] because the client options are
+ * long-lived and usually should not be synchronously closed via try-with-resources.
+ *
+ * It's also usually not necessary to call this method at all. the default client automatically
+ * releases threads and connections if they remain idle, but if you are writing an application
+ * that needs to aggressively release unused resources, then you may call this method.
+ */
+ fun close() {
+ httpClient.close()
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/ObjectMappers.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/ObjectMappers.kt
new file mode 100644
index 0000000..beaef0f
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/ObjectMappers.kt
@@ -0,0 +1,167 @@
+@file:JvmName("ObjectMappers")
+
+package com.imagekit.api.core
+
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.JsonParseException
+import com.fasterxml.jackson.core.JsonParser
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.MapperFeature
+import com.fasterxml.jackson.databind.SerializationFeature
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.cfg.CoercionAction
+import com.fasterxml.jackson.databind.cfg.CoercionInputShape
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer
+import com.fasterxml.jackson.databind.json.JsonMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import com.fasterxml.jackson.databind.type.LogicalType
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
+import com.fasterxml.jackson.module.kotlin.kotlinModule
+import java.io.InputStream
+import java.time.DateTimeException
+import java.time.LocalDate
+import java.time.LocalDateTime
+import java.time.ZonedDateTime
+import java.time.format.DateTimeFormatter
+import java.time.temporal.ChronoField
+
+fun jsonMapper(): JsonMapper =
+ JsonMapper.builder()
+ .addModule(kotlinModule())
+ .addModule(Jdk8Module())
+ .addModule(JavaTimeModule())
+ .addModule(
+ SimpleModule()
+ .addSerializer(InputStreamSerializer)
+ .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer())
+ )
+ .withCoercionConfig(LogicalType.Boolean) {
+ it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Integer) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Float) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Textual) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Array) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Collection) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.Map) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Object, CoercionAction.Fail)
+ }
+ .withCoercionConfig(LogicalType.POJO) {
+ it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.String, CoercionAction.Fail)
+ .setCoercion(CoercionInputShape.Array, CoercionAction.Fail)
+ }
+ .serializationInclusion(JsonInclude.Include.NON_ABSENT)
+ .disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
+ .disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
+ .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
+ .disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
+ .disable(MapperFeature.ALLOW_COERCION_OF_SCALARS)
+ .disable(MapperFeature.AUTO_DETECT_CREATORS)
+ .disable(MapperFeature.AUTO_DETECT_FIELDS)
+ .disable(MapperFeature.AUTO_DETECT_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_IS_GETTERS)
+ .disable(MapperFeature.AUTO_DETECT_SETTERS)
+ .build()
+
+/** A serializer that serializes [InputStream] to bytes. */
+private object InputStreamSerializer : BaseSerializer(InputStream::class) {
+
+ private fun readResolve(): Any = InputStreamSerializer
+
+ override fun serialize(
+ value: InputStream?,
+ gen: JsonGenerator?,
+ serializers: SerializerProvider?,
+ ) {
+ if (value == null) {
+ gen?.writeNull()
+ } else {
+ value.use { gen?.writeBinary(it.readBytes()) }
+ }
+ }
+}
+
+/**
+ * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes.
+ */
+private class LenientLocalDateTimeDeserializer :
+ StdDeserializer(LocalDateTime::class.java) {
+
+ companion object {
+
+ private val DATE_TIME_FORMATTERS =
+ listOf(
+ DateTimeFormatter.ISO_LOCAL_DATE_TIME,
+ DateTimeFormatter.ISO_LOCAL_DATE,
+ DateTimeFormatter.ISO_ZONED_DATE_TIME,
+ )
+ }
+
+ override fun logicalType(): LogicalType = LogicalType.DateTime
+
+ override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime {
+ val exceptions = mutableListOf()
+
+ for (formatter in DATE_TIME_FORMATTERS) {
+ try {
+ val temporal = formatter.parse(p.text)
+
+ return when {
+ !temporal.isSupported(ChronoField.HOUR_OF_DAY) ->
+ LocalDate.from(temporal).atStartOfDay()
+ !temporal.isSupported(ChronoField.OFFSET_SECONDS) ->
+ LocalDateTime.from(temporal)
+ else -> ZonedDateTime.from(temporal).toLocalDateTime()
+ }
+ } catch (e: DateTimeException) {
+ exceptions.add(e)
+ }
+ }
+
+ throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply {
+ exceptions.forEach { addSuppressed(it) }
+ }
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Params.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Params.kt
new file mode 100644
index 0000000..2a62a93
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Params.kt
@@ -0,0 +1,16 @@
+package com.imagekit.api.core
+
+import com.imagekit.api.core.http.Headers
+import com.imagekit.api.core.http.QueryParams
+
+/** An interface representing parameters passed to a service method. */
+interface Params {
+ /** The full set of headers in the parameters, including both fixed and additional headers. */
+ fun _headers(): Headers
+
+ /**
+ * The full set of query params in the parameters, including both fixed and additional query
+ * params.
+ */
+ fun _queryParams(): QueryParams
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PhantomReachable.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PhantomReachable.kt
new file mode 100644
index 0000000..d17a689
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PhantomReachable.kt
@@ -0,0 +1,56 @@
+@file:JvmName("PhantomReachable")
+
+package com.imagekit.api.core
+
+import com.imagekit.api.errors.ImageKitException
+import java.lang.reflect.InvocationTargetException
+
+/**
+ * Closes [closeable] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, closeable: AutoCloseable) {
+ check(observed !== closeable) {
+ "`observed` cannot be the same object as `closeable` because it would never become phantom reachable"
+ }
+ closeWhenPhantomReachable(observed, closeable::close)
+}
+
+/**
+ * Calls [close] when [observed] becomes only phantom reachable.
+ *
+ * This is a wrapper around a Java 9+ [java.lang.ref.Cleaner], or a no-op in older Java versions.
+ */
+@JvmSynthetic
+internal fun closeWhenPhantomReachable(observed: Any, close: () -> Unit) {
+ closeWhenPhantomReachable?.let { it(observed, close) }
+}
+
+private val closeWhenPhantomReachable: ((Any, () -> Unit) -> Unit)? by lazy {
+ try {
+ val cleanerClass = Class.forName("java.lang.ref.Cleaner")
+ val cleanerCreate = cleanerClass.getMethod("create")
+ val cleanerRegister =
+ cleanerClass.getMethod("register", Any::class.java, Runnable::class.java)
+ val cleanerObject = cleanerCreate.invoke(null);
+
+ { observed, close ->
+ try {
+ cleanerRegister.invoke(cleanerObject, observed, Runnable { close() })
+ } catch (e: ReflectiveOperationException) {
+ if (e is InvocationTargetException) {
+ when (val cause = e.cause) {
+ is RuntimeException,
+ is Error -> throw cause
+ }
+ }
+ throw ImageKitException("Unexpected reflective invocation failure", e)
+ }
+ }
+ } catch (e: ReflectiveOperationException) {
+ // We're running Java 8, which has no Cleaner.
+ null
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PhantomReachableExecutorService.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PhantomReachableExecutorService.kt
new file mode 100644
index 0000000..19a3538
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PhantomReachableExecutorService.kt
@@ -0,0 +1,58 @@
+package com.imagekit.api.core
+
+import java.util.concurrent.Callable
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+
+/**
+ * A delegating wrapper around an [ExecutorService] that shuts it down once it's only phantom
+ * reachable.
+ *
+ * This class ensures the [ExecutorService] is shut down even if the user forgets to do it.
+ */
+internal class PhantomReachableExecutorService(private val executorService: ExecutorService) :
+ ExecutorService {
+ init {
+ closeWhenPhantomReachable(this) { executorService.shutdown() }
+ }
+
+ override fun execute(command: Runnable) = executorService.execute(command)
+
+ override fun shutdown() = executorService.shutdown()
+
+ override fun shutdownNow(): MutableList = executorService.shutdownNow()
+
+ override fun isShutdown(): Boolean = executorService.isShutdown
+
+ override fun isTerminated(): Boolean = executorService.isTerminated
+
+ override fun awaitTermination(timeout: Long, unit: TimeUnit): Boolean =
+ executorService.awaitTermination(timeout, unit)
+
+ override fun submit(task: Callable): Future = executorService.submit(task)
+
+ override fun submit(task: Runnable, result: T): Future =
+ executorService.submit(task, result)
+
+ override fun submit(task: Runnable): Future<*> = executorService.submit(task)
+
+ override fun invokeAll(
+ tasks: MutableCollection>
+ ): MutableList> = executorService.invokeAll(tasks)
+
+ override fun invokeAll(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): MutableList> = executorService.invokeAll(tasks, timeout, unit)
+
+ override fun invokeAny(tasks: MutableCollection>): T =
+ executorService.invokeAny(tasks)
+
+ override fun invokeAny(
+ tasks: MutableCollection>,
+ timeout: Long,
+ unit: TimeUnit,
+ ): T = executorService.invokeAny(tasks, timeout, unit)
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PrepareRequest.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PrepareRequest.kt
new file mode 100644
index 0000000..5ea20e4
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/PrepareRequest.kt
@@ -0,0 +1,24 @@
+@file:JvmName("PrepareRequest")
+
+package com.imagekit.api.core
+
+import com.imagekit.api.core.http.HttpRequest
+import java.util.concurrent.CompletableFuture
+
+@JvmSynthetic
+internal fun HttpRequest.prepare(clientOptions: ClientOptions, params: Params): HttpRequest =
+ toBuilder()
+ .putAllQueryParams(clientOptions.queryParams)
+ .replaceAllQueryParams(params._queryParams())
+ .putAllHeaders(clientOptions.headers)
+ .replaceAllHeaders(params._headers())
+ .build()
+
+@JvmSynthetic
+internal fun HttpRequest.prepareAsync(
+ clientOptions: ClientOptions,
+ params: Params,
+): CompletableFuture =
+ // This async version exists to make it easier to add async specific preparation logic in the
+ // future.
+ CompletableFuture.completedFuture(prepare(clientOptions, params))
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Properties.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Properties.kt
new file mode 100644
index 0000000..bc15f01
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Properties.kt
@@ -0,0 +1,42 @@
+@file:JvmName("Properties")
+
+package com.imagekit.api.core
+
+import java.util.Properties
+
+fun getOsArch(): String {
+ val osArch = System.getProperty("os.arch")
+
+ return when (osArch) {
+ null -> "unknown"
+ "i386",
+ "x32",
+ "x86" -> "x32"
+ "amd64",
+ "x86_64" -> "x64"
+ "arm" -> "arm"
+ "aarch64" -> "arm64"
+ else -> "other:${osArch}"
+ }
+}
+
+fun getOsName(): String {
+ val osName = System.getProperty("os.name")
+ val vendorUrl = System.getProperty("java.vendor.url")
+
+ return when {
+ osName == null -> "Unknown"
+ osName.startsWith("Linux") && vendorUrl == "http://www.android.com/" -> "Android"
+ osName.startsWith("Linux") -> "Linux"
+ osName.startsWith("Mac OS") -> "MacOS"
+ osName.startsWith("Windows") -> "Windows"
+ else -> "Other:${osName}"
+ }
+}
+
+fun getOsVersion(): String = System.getProperty("os.version", "unknown")
+
+fun getPackageVersion(): String =
+ Properties::class.java.`package`.implementationVersion ?: "unknown"
+
+fun getJavaVersion(): String = System.getProperty("java.version", "unknown")
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/RequestOptions.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/RequestOptions.kt
new file mode 100644
index 0000000..2d4840a
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/RequestOptions.kt
@@ -0,0 +1,46 @@
+package com.imagekit.api.core
+
+import java.time.Duration
+
+class RequestOptions private constructor(val responseValidation: Boolean?, val timeout: Timeout?) {
+
+ companion object {
+
+ private val NONE = builder().build()
+
+ @JvmStatic fun none() = NONE
+
+ @JvmSynthetic
+ internal fun from(clientOptions: ClientOptions): RequestOptions =
+ builder()
+ .responseValidation(clientOptions.responseValidation)
+ .timeout(clientOptions.timeout)
+ .build()
+
+ @JvmStatic fun builder() = Builder()
+ }
+
+ fun applyDefaults(options: RequestOptions): RequestOptions =
+ RequestOptions(
+ responseValidation = responseValidation ?: options.responseValidation,
+ timeout =
+ if (options.timeout != null && timeout != null) timeout.assign(options.timeout)
+ else timeout ?: options.timeout,
+ )
+
+ class Builder internal constructor() {
+
+ private var responseValidation: Boolean? = null
+ private var timeout: Timeout? = null
+
+ fun responseValidation(responseValidation: Boolean) = apply {
+ this.responseValidation = responseValidation
+ }
+
+ fun timeout(timeout: Timeout) = apply { this.timeout = timeout }
+
+ fun timeout(timeout: Duration) = timeout(Timeout.builder().request(timeout).build())
+
+ fun build(): RequestOptions = RequestOptions(responseValidation, timeout)
+ }
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Timeout.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Timeout.kt
new file mode 100644
index 0000000..5ca5989
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Timeout.kt
@@ -0,0 +1,171 @@
+// File generated from our OpenAPI spec by Stainless.
+
+package com.imagekit.api.core
+
+import java.time.Duration
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+
+/** A class containing timeouts for various processing phases of a request. */
+class Timeout
+private constructor(
+ private val connect: Duration?,
+ private val read: Duration?,
+ private val write: Duration?,
+ private val request: Duration?,
+) {
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(): Duration = connect ?: Duration.ofMinutes(1)
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(): Duration = read ?: request()
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(): Duration = write ?: request()
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as well
+ * as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(): Duration = request ?: Duration.ofMinutes(1)
+
+ fun toBuilder() = Builder().from(this)
+
+ companion object {
+
+ @JvmStatic fun default() = builder().build()
+
+ /** Returns a mutable builder for constructing an instance of [Timeout]. */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** A builder for [Timeout]. */
+ class Builder internal constructor() {
+
+ private var connect: Duration? = null
+ private var read: Duration? = null
+ private var write: Duration? = null
+ private var request: Duration? = null
+
+ @JvmSynthetic
+ internal fun from(timeout: Timeout) = apply {
+ connect = timeout.connect
+ read = timeout.read
+ write = timeout.write
+ request = timeout.request
+ }
+
+ /**
+ * The maximum time allowed to establish a connection with a host.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun connect(connect: Duration?) = apply { this.connect = connect }
+
+ /** Alias for calling [Builder.connect] with `connect.orElse(null)`. */
+ fun connect(connect: Optional) = connect(connect.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when waiting for the server’s response.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun read(read: Duration?) = apply { this.read = read }
+
+ /** Alias for calling [Builder.read] with `read.orElse(null)`. */
+ fun read(read: Optional) = read(read.getOrNull())
+
+ /**
+ * The maximum time allowed between two data packets when sending the request to the server.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `request()`.
+ */
+ fun write(write: Duration?) = apply { this.write = write }
+
+ /** Alias for calling [Builder.write] with `write.orElse(null)`. */
+ fun write(write: Optional) = write(write.getOrNull())
+
+ /**
+ * The maximum time allowed for a complete HTTP call, not including retries.
+ *
+ * This includes resolving DNS, connecting, writing the request body, server processing, as
+ * well as reading the response body.
+ *
+ * A value of [Duration.ZERO] means there's no timeout.
+ *
+ * Defaults to `Duration.ofMinutes(1)`.
+ */
+ fun request(request: Duration?) = apply { this.request = request }
+
+ /** Alias for calling [Builder.request] with `request.orElse(null)`. */
+ fun request(request: Optional) = request(request.getOrNull())
+
+ /**
+ * Returns an immutable instance of [Timeout].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ */
+ fun build(): Timeout = Timeout(connect, read, write, request)
+ }
+
+ @JvmSynthetic
+ internal fun assign(target: Timeout): Timeout =
+ target
+ .toBuilder()
+ .apply {
+ connect?.let(this::connect)
+ read?.let(this::read)
+ write?.let(this::write)
+ request?.let(this::request)
+ }
+ .build()
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is Timeout &&
+ connect == other.connect &&
+ read == other.read &&
+ write == other.write &&
+ request == other.request
+ }
+
+ override fun hashCode(): Int = Objects.hash(connect, read, write, request)
+
+ override fun toString() =
+ "Timeout{connect=$connect, read=$read, write=$write, request=$request}"
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Utils.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Utils.kt
new file mode 100644
index 0000000..2076536
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Utils.kt
@@ -0,0 +1,115 @@
+@file:JvmName("Utils")
+
+package com.imagekit.api.core
+
+import com.imagekit.api.errors.ImageKitInvalidDataException
+import java.util.Collections
+import java.util.SortedMap
+import java.util.concurrent.CompletableFuture
+import java.util.concurrent.locks.Lock
+
+@JvmSynthetic
+internal fun T?.getOrThrow(name: String): T =
+ this ?: throw ImageKitInvalidDataException("`${name}` is not present")
+
+@JvmSynthetic
+internal fun List.toImmutable(): List =
+ if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
+
+@JvmSynthetic
+internal fun Map.toImmutable(): Map =
+ if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())
+
+@JvmSynthetic internal fun immutableEmptyMap(): Map = Collections.emptyMap()
+
+@JvmSynthetic
+internal fun , V> SortedMap.toImmutable(): SortedMap =
+ if (isEmpty()) Collections.emptySortedMap()
+ else Collections.unmodifiableSortedMap(toSortedMap(comparator()))
+
+/**
+ * Returns all elements that yield the largest value for the given function, or an empty list if
+ * there are zero elements.
+ *
+ * This is similar to [Sequence.maxByOrNull] except it returns _all_ elements that yield the largest
+ * value; not just the first one.
+ */
+@JvmSynthetic
+internal fun > Sequence.allMaxBy(selector: (T) -> R): List {
+ var maxValue: R? = null
+ val maxElements = mutableListOf()
+
+ val iterator = iterator()
+ while (iterator.hasNext()) {
+ val element = iterator.next()
+ val value = selector(element)
+ if (maxValue == null || value > maxValue) {
+ maxValue = value
+ maxElements.clear()
+ maxElements.add(element)
+ } else if (value == maxValue) {
+ maxElements.add(element)
+ }
+ }
+
+ return maxElements
+}
+
+/**
+ * Returns whether [this] is equal to [other].
+ *
+ * This differs from [Object.equals] because it also deeply equates arrays based on their contents,
+ * even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal infix fun Any?.contentEquals(other: Any?): Boolean =
+ arrayOf(this).contentDeepEquals(arrayOf(other))
+
+/**
+ * Returns a hash of the given sequence of [values].
+ *
+ * This differs from [java.util.Objects.hash] because it also deeply hashes arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic internal fun contentHash(vararg values: Any?): Int = values.contentDeepHashCode()
+
+/**
+ * Returns a [String] representation of [this].
+ *
+ * This differs from [Object.toString] because it also deeply stringifies arrays based on their
+ * contents, even when there are arrays directly nested within other arrays.
+ */
+@JvmSynthetic
+internal fun Any?.contentToString(): String {
+ var string = arrayOf(this).contentDeepToString()
+ if (string.startsWith('[')) {
+ string = string.substring(1)
+ }
+ if (string.endsWith(']')) {
+ string = string.substring(0, string.length - 1)
+ }
+ return string
+}
+
+internal interface Enum
+
+/**
+ * Executes the given [action] while holding the lock, returning a [CompletableFuture] with the
+ * result.
+ *
+ * @param action The asynchronous action to execute while holding the lock
+ * @return A [CompletableFuture] that completes with the result of the action
+ */
+@JvmSynthetic
+internal fun Lock.withLockAsync(action: () -> CompletableFuture): CompletableFuture {
+ lock()
+ val future =
+ try {
+ action()
+ } catch (e: Throwable) {
+ unlock()
+ throw e
+ }
+ future.whenComplete { _, _ -> unlock() }
+ return future
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt
new file mode 100644
index 0000000..43bda41
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/Values.kt
@@ -0,0 +1,723 @@
+package com.imagekit.api.core
+
+import com.fasterxml.jackson.annotation.JacksonAnnotationsInside
+import com.fasterxml.jackson.annotation.JsonCreator
+import com.fasterxml.jackson.annotation.JsonInclude
+import com.fasterxml.jackson.core.JsonGenerator
+import com.fasterxml.jackson.core.ObjectCodec
+import com.fasterxml.jackson.core.type.TypeReference
+import com.fasterxml.jackson.databind.BeanProperty
+import com.fasterxml.jackson.databind.DeserializationContext
+import com.fasterxml.jackson.databind.JavaType
+import com.fasterxml.jackson.databind.JsonDeserializer
+import com.fasterxml.jackson.databind.JsonNode
+import com.fasterxml.jackson.databind.SerializerProvider
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize
+import com.fasterxml.jackson.databind.annotation.JsonSerialize
+import com.fasterxml.jackson.databind.node.JsonNodeType.ARRAY
+import com.fasterxml.jackson.databind.node.JsonNodeType.BINARY
+import com.fasterxml.jackson.databind.node.JsonNodeType.BOOLEAN
+import com.fasterxml.jackson.databind.node.JsonNodeType.MISSING
+import com.fasterxml.jackson.databind.node.JsonNodeType.NULL
+import com.fasterxml.jackson.databind.node.JsonNodeType.NUMBER
+import com.fasterxml.jackson.databind.node.JsonNodeType.OBJECT
+import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
+import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
+import com.fasterxml.jackson.databind.ser.std.NullSerializer
+import com.imagekit.api.errors.ImageKitInvalidDataException
+import java.io.InputStream
+import java.util.Objects
+import java.util.Optional
+
+/**
+ * A class representing a serializable JSON field.
+ *
+ * It can either be a [KnownValue] value of type [T], matching the type the SDK expects, or an
+ * arbitrary JSON value that bypasses the type system (via [JsonValue]).
+ */
+@JsonDeserialize(using = JsonField.Deserializer::class)
+sealed class JsonField {
+
+ /**
+ * Returns whether this field is missing, which means it will be omitted from the serialized
+ * JSON entirely.
+ */
+ fun isMissing(): Boolean = this is JsonMissing
+
+ /** Whether this field is explicitly set to `null`. */
+ fun isNull(): Boolean = this is JsonNull
+
+ /**
+ * Returns an [Optional] containing this field's "known" value, meaning it matches the type the
+ * SDK expects, or an empty [Optional] if this field contains an arbitrary [JsonValue].
+ *
+ * This is the opposite of [asUnknown].
+ */
+ fun asKnown():
+ Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > = Optional.ofNullable((this as? KnownValue)?.value)
+
+ /**
+ * Returns an [Optional] containing this field's arbitrary [JsonValue], meaning it mismatches
+ * the type the SDK expects, or an empty [Optional] if this field contains a "known" value.
+ *
+ * This is the opposite of [asKnown].
+ */
+ fun asUnknown(): Optional = Optional.ofNullable(this as? JsonValue)
+
+ /**
+ * Returns an [Optional] containing this field's boolean value, or an empty [Optional] if it
+ * doesn't contain a boolean.
+ *
+ * This method checks for both a [KnownValue] containing a boolean and for [JsonBoolean].
+ */
+ fun asBoolean(): Optional =
+ when (this) {
+ is JsonBoolean -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Boolean)
+ else -> Optional.empty()
+ }
+
+ /**
+ * Returns an [Optional] containing this field's numerical value, or an empty [Optional] if it
+ * doesn't contain a number.
+ *
+ * This method checks for both a [KnownValue] containing a number and for [JsonNumber].
+ */
+ fun asNumber(): Optional =
+ when (this) {
+ is JsonNumber -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? Number)
+ else -> Optional.empty()
+ }
+
+ /**
+ * Returns an [Optional] containing this field's string value, or an empty [Optional] if it
+ * doesn't contain a string.
+ *
+ * This method checks for both a [KnownValue] containing a string and for [JsonString].
+ */
+ fun asString(): Optional =
+ when (this) {
+ is JsonString -> Optional.of(value)
+ is KnownValue -> Optional.ofNullable(value as? String)
+ else -> Optional.empty()
+ }
+
+ fun asStringOrThrow(): String =
+ asString().orElseThrow { ImageKitInvalidDataException("Value is not a string") }
+
+ /**
+ * Returns an [Optional] containing this field's list value, or an empty [Optional] if it
+ * doesn't contain a list.
+ *
+ * This method checks for both a [KnownValue] containing a list and for [JsonArray].
+ */
+ fun asArray(): Optional> =
+ when (this) {
+ is JsonArray -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? List<*>)?.map {
+ try {
+ JsonValue.from(it)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a list, but not all values are convertible to
+ // `JsonValue`.
+ return Optional.empty()
+ }
+ }
+ )
+ else -> Optional.empty()
+ }
+
+ /**
+ * Returns an [Optional] containing this field's map value, or an empty [Optional] if it doesn't
+ * contain a map.
+ *
+ * This method checks for both a [KnownValue] containing a map and for [JsonObject].
+ */
+ fun asObject(): Optional> =
+ when (this) {
+ is JsonObject -> Optional.of(values)
+ is KnownValue ->
+ Optional.ofNullable(
+ (value as? Map<*, *>)
+ ?.map { (key, value) ->
+ if (key !is String) {
+ return Optional.empty()
+ }
+
+ val jsonValue =
+ try {
+ JsonValue.from(value)
+ } catch (e: IllegalArgumentException) {
+ // The known value is a map, but not all items are convertible
+ // to `JsonValue`.
+ return Optional.empty()
+ }
+
+ key to jsonValue
+ }
+ ?.toMap()
+ )
+ else -> Optional.empty()
+ }
+
+ @JvmSynthetic
+ internal fun getRequired(name: String): T =
+ when (this) {
+ is KnownValue -> value
+ is JsonMissing -> throw ImageKitInvalidDataException("`$name` is not set")
+ is JsonNull -> throw ImageKitInvalidDataException("`$name` is null")
+ else -> throw ImageKitInvalidDataException("`$name` is invalid, received $this")
+ }
+
+ @JvmSynthetic
+ internal fun getOptional(
+ name: String
+ ): Optional<
+ // Safe because `Optional` is effectively covariant, but Kotlin doesn't know that.
+ @UnsafeVariance
+ T
+ > =
+ when (this) {
+ is KnownValue -> Optional.of(value)
+ is JsonMissing,
+ is JsonNull -> Optional.empty()
+ else -> throw ImageKitInvalidDataException("`$name` is invalid, received $this")
+ }
+
+ @JvmSynthetic
+ internal fun map(transform: (T) -> R): JsonField =
+ when (this) {
+ is KnownValue -> KnownValue.of(transform(value))
+ is JsonValue -> this
+ }
+
+ @JvmSynthetic internal fun accept(consume: (T) -> Unit) = asKnown().ifPresent(consume)
+
+ /** Returns the result of calling the [visitor] method corresponding to this field's state. */
+ fun accept(visitor: Visitor): R =
+ when (this) {
+ is KnownValue -> visitor.visitKnown(value)
+ is JsonValue -> accept(visitor as JsonValue.Visitor)
+ }
+
+ /**
+ * An interface that defines how to map each possible state of a `JsonField` to a value of
+ * type [R].
+ */
+ interface Visitor : JsonValue.Visitor {
+
+ fun visitKnown(value: T): R = visitDefault()
+ }
+
+ companion object {
+
+ /** Returns a [JsonField] containing the given "known" [value]. */
+ @JvmStatic fun of(value: T): JsonField = KnownValue.of(value)
+
+ /**
+ * Returns a [JsonField] containing the given "known" [value], or [JsonNull] if [value] is
+ * null.
+ */
+ @JvmStatic
+ fun ofNullable(value: T?): JsonField =
+ when (value) {
+ null -> JsonNull.of()
+ else -> KnownValue.of(value)
+ }
+ }
+
+ /**
+ * This class is a Jackson filter that can be used to exclude missing properties from objects.
+ * This filter should not be used directly and should instead use the @ExcludeMissing
+ * annotation.
+ */
+ class IsMissing {
+
+ override fun equals(other: Any?): Boolean = other is JsonMissing
+
+ override fun hashCode(): Int = Objects.hash()
+ }
+
+ class Deserializer(private val type: JavaType? = null) :
+ BaseDeserializer>(JsonField::class) {
+
+ override fun createContextual(
+ context: DeserializationContext,
+ property: BeanProperty?,
+ ): JsonDeserializer> = Deserializer(context.contextualType?.containedType(0))
+
+ override fun ObjectCodec.deserialize(node: JsonNode): JsonField<*> =
+ type?.let { tryDeserialize(node, type) }?.let { of(it) }
+ ?: JsonValue.fromJsonNode(node)
+
+ override fun getNullValue(context: DeserializationContext): JsonField<*> = JsonNull.of()
+ }
+}
+
+/**
+ * A class representing an arbitrary JSON value.
+ *
+ * It is immutable and assignable to any [JsonField], regardless of its expected type (i.e. its
+ * generic type argument).
+ */
+@JsonDeserialize(using = JsonValue.Deserializer::class)
+sealed class JsonValue : JsonField() {
+
+ fun convert(type: TypeReference): R? = JSON_MAPPER.convertValue(this, type)
+
+ fun convert(type: Class): R? = JSON_MAPPER.convertValue(this, type)
+
+ /** Returns the result of calling the [visitor] method corresponding to this value's variant. */
+ fun accept(visitor: Visitor): R =
+ when (this) {
+ is JsonMissing -> visitor.visitMissing()
+ is JsonNull -> visitor.visitNull()
+ is JsonBoolean -> visitor.visitBoolean(value)
+ is JsonNumber -> visitor.visitNumber(value)
+ is JsonString -> visitor.visitString(value)
+ is JsonArray -> visitor.visitArray(values)
+ is JsonObject -> visitor.visitObject(values)
+ }
+
+ /**
+ * An interface that defines how to map each variant state of a [JsonValue] to a value of type
+ * [R].
+ */
+ interface Visitor {
+
+ fun visitNull(): R = visitDefault()
+
+ fun visitMissing(): R = visitDefault()
+
+ fun visitBoolean(value: Boolean): R = visitDefault()
+
+ fun visitNumber(value: Number): R = visitDefault()
+
+ fun visitString(value: String): R = visitDefault()
+
+ fun visitArray(values: List): R = visitDefault()
+
+ fun visitObject(values: Map): R = visitDefault()
+
+ /**
+ * The default implementation for unimplemented visitor methods.
+ *
+ * @throws IllegalArgumentException in the default implementation.
+ */
+ fun visitDefault(): R = throw IllegalArgumentException("Unexpected value")
+ }
+
+ companion object {
+
+ private val JSON_MAPPER = jsonMapper()
+
+ /**
+ * Converts the given [value] to a [JsonValue].
+ *
+ * This method works best on primitive types, [List] values, [Map] values, and nested
+ * combinations of these. For example:
+ * ```java
+ * // Create primitive JSON values
+ * JsonValue nullValue = JsonValue.from(null);
+ * JsonValue booleanValue = JsonValue.from(true);
+ * JsonValue numberValue = JsonValue.from(42);
+ * JsonValue stringValue = JsonValue.from("Hello World!");
+ *
+ * // Create a JSON array value equivalent to `["Hello", "World"]`
+ * JsonValue arrayValue = JsonValue.from(List.of("Hello", "World"));
+ *
+ * // Create a JSON object value equivalent to `{ "a": 1, "b": 2 }`
+ * JsonValue objectValue = JsonValue.from(Map.of(
+ * "a", 1,
+ * "b", 2
+ * ));
+ *
+ * // Create an arbitrarily nested JSON equivalent to:
+ * // {
+ * // "a": [1, 2],
+ * // "b": [3, 4]
+ * // }
+ * JsonValue complexValue = JsonValue.from(Map.of(
+ * "a", List.of(1, 2),
+ * "b", List.of(3, 4)
+ * ));
+ * ```
+ *
+ * @throws IllegalArgumentException if [value] is not JSON serializable.
+ */
+ @JvmStatic
+ fun from(value: Any?): JsonValue =
+ when (value) {
+ null -> JsonNull.of()
+ is JsonValue -> value
+ else -> JSON_MAPPER.convertValue(value, JsonValue::class.java)
+ }
+
+ /**
+ * Returns a [JsonValue] converted from the given Jackson [JsonNode].
+ *
+ * @throws IllegalStateException for unsupported node types.
+ */
+ @JvmStatic
+ fun fromJsonNode(node: JsonNode): JsonValue =
+ when (node.nodeType) {
+ MISSING -> JsonMissing.of()
+ NULL -> JsonNull.of()
+ BOOLEAN -> JsonBoolean.of(node.booleanValue())
+ NUMBER -> JsonNumber.of(node.numberValue())
+ STRING -> JsonString.of(node.textValue())
+ ARRAY ->
+ JsonArray.of(node.elements().asSequence().map { fromJsonNode(it) }.toList())
+ OBJECT ->
+ JsonObject.of(
+ node.fields().asSequence().map { it.key to fromJsonNode(it.value) }.toMap()
+ )
+ BINARY,
+ POJO,
+ null -> throw IllegalStateException("Unexpected JsonNode type: ${node.nodeType}")
+ }
+ }
+
+ class Deserializer : BaseDeserializer(JsonValue::class) {
+
+ override fun ObjectCodec.deserialize(node: JsonNode): JsonValue = fromJsonNode(node)
+
+ override fun getNullValue(context: DeserializationContext?): JsonValue = JsonNull.of()
+ }
+}
+
+/**
+ * A class representing a "known" JSON serializable value of type [T], matching the type the SDK
+ * expects.
+ *
+ * It is assignable to `JsonField`.
+ */
+class KnownValue
+private constructor(
+ @com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: T
+) : JsonField() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is KnownValue<*> && value contentEquals other.value
+ }
+
+ override fun hashCode() = contentHash(value)
+
+ override fun toString() = value.contentToString()
+
+ companion object {
+
+ /** Returns a [KnownValue] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: T) = KnownValue(value)
+ }
+}
+
+/**
+ * A [JsonValue] representing an omitted JSON field.
+ *
+ * An instance of this class will cause a JSON field to be omitted from the serialized JSON
+ * entirely.
+ */
+@JsonSerialize(using = JsonMissing.Serializer::class)
+class JsonMissing : JsonValue() {
+
+ override fun toString() = ""
+
+ companion object {
+
+ private val INSTANCE: JsonMissing = JsonMissing()
+
+ /** Returns the singleton instance of [JsonMissing]. */
+ @JvmStatic fun of() = INSTANCE
+ }
+
+ class Serializer : BaseSerializer(JsonMissing::class) {
+
+ override fun serialize(
+ value: JsonMissing,
+ generator: JsonGenerator,
+ provider: SerializerProvider,
+ ) {
+ throw IllegalStateException("JsonMissing cannot be serialized")
+ }
+ }
+}
+
+/** A [JsonValue] representing a JSON `null` value. */
+@JsonSerialize(using = NullSerializer::class)
+class JsonNull : JsonValue() {
+
+ override fun toString() = "null"
+
+ companion object {
+
+ private val INSTANCE: JsonNull = JsonNull()
+
+ /** Returns the singleton instance of [JsonMissing]. */
+ @JsonCreator @JvmStatic fun of() = INSTANCE
+ }
+}
+
+/** A [JsonValue] representing a JSON boolean value. */
+class JsonBoolean
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Boolean
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonBoolean && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+
+ companion object {
+
+ /** Returns a [JsonBoolean] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: Boolean) = JsonBoolean(value)
+ }
+}
+
+/** A [JsonValue] representing a JSON number value. */
+class JsonNumber
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: Number
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonNumber && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value.toString()
+
+ companion object {
+
+ /** Returns a [JsonNumber] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: Number) = JsonNumber(value)
+ }
+}
+
+/** A [JsonValue] representing a JSON string value. */
+class JsonString
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: String
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonString && value == other.value
+ }
+
+ override fun hashCode() = value.hashCode()
+
+ override fun toString() = value
+
+ companion object {
+
+ /** Returns a [JsonString] containing the given [value]. */
+ @JsonCreator @JvmStatic fun of(value: String) = JsonString(value)
+ }
+}
+
+/** A [JsonValue] representing a JSON array value. */
+class JsonArray
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue
+ @get:JvmName("values")
+ val values: List
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonArray && values == other.values
+ }
+
+ override fun hashCode() = values.hashCode()
+
+ override fun toString() = values.toString()
+
+ companion object {
+
+ /** Returns a [JsonArray] containing the given [values]. */
+ @JsonCreator @JvmStatic fun of(values: List) = JsonArray(values.toImmutable())
+ }
+}
+
+/** A [JsonValue] representing a JSON object value. */
+class JsonObject
+private constructor(
+ @get:com.fasterxml.jackson.annotation.JsonValue
+ @get:JvmName("values")
+ val values: Map
+) : JsonValue() {
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is JsonObject && values == other.values
+ }
+
+ override fun hashCode() = values.hashCode()
+
+ override fun toString() = values.toString()
+
+ companion object {
+
+ /** Returns a [JsonObject] containing the given [values]. */
+ @JsonCreator
+ @JvmStatic
+ fun of(values: Map) = JsonObject(values.toImmutable())
+ }
+}
+
+/** A Jackson annotation for excluding fields set to [JsonMissing] from the serialized JSON. */
+@JacksonAnnotationsInside
+@JsonInclude(JsonInclude.Include.CUSTOM, valueFilter = JsonField.IsMissing::class)
+annotation class ExcludeMissing
+
+/** A class representing a field in a `multipart/form-data` request. */
+class MultipartField
+private constructor(
+ /** A [JsonField] value, which will be serialized to zero or more parts. */
+ @get:com.fasterxml.jackson.annotation.JsonValue @get:JvmName("value") val value: JsonField,
+ /** A content type for the serialized parts. */
+ @get:JvmName("contentType") val contentType: String,
+ private val filename: String?,
+) {
+
+ companion object {
+
+ /**
+ * Returns a [MultipartField] containing the given [value] as a [KnownValue].
+ *
+ * [contentType] will be set to `application/octet-stream` if [value] is binary data, or
+ * `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun of(value: T?) = builder().value(value).build()
+
+ /**
+ * Returns a [MultipartField] containing the given [value].
+ *
+ * [contentType] will be set to `application/octet-stream` if [value] is binary data, or
+ * `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun of(value: JsonField) = builder().value(value).build()
+
+ /**
+ * Returns a mutable builder for constructing an instance of [MultipartField].
+ *
+ * The following fields are required:
+ * ```java
+ * .value()
+ * ```
+ *
+ * If [contentType] is unset, then it will be set to `application/octet-stream` if [value]
+ * is binary data, or `text/plain; charset=utf-8` otherwise.
+ */
+ @JvmStatic fun builder() = Builder()
+ }
+
+ /** Returns the filename directive that will be included in the serialized field. */
+ fun filename(): Optional = Optional.ofNullable(filename)
+
+ @JvmSynthetic
+ internal fun map(transform: (T) -> R): MultipartField =
+ builder().value(value.map(transform)).contentType(contentType).filename(filename).build()
+
+ /** A builder for [MultipartField]. */
+ class Builder internal constructor() {
+
+ private var value: JsonField? = null
+ private var contentType: String? = null
+ private var filename: String? = null
+
+ fun value(value: JsonField) = apply { this.value = value }
+
+ fun value(value: T?) = value(JsonField.ofNullable(value))
+
+ fun contentType(contentType: String) = apply { this.contentType = contentType }
+
+ fun filename(filename: String?) = apply { this.filename = filename }
+
+ /** Alias for calling [Builder.filename] with `filename.orElse(null)`. */
+ fun filename(filename: Optional) = filename(filename.orElse(null))
+
+ /**
+ * Returns an immutable instance of [MultipartField].
+ *
+ * Further updates to this [Builder] will not mutate the returned instance.
+ *
+ * The following fields are required:
+ * ```java
+ * .value()
+ * ```
+ *
+ * If [contentType] is unset, then it will be set to `application/octet-stream` if [value]
+ * is binary data, or `text/plain; charset=utf-8` otherwise.
+ *
+ * @throws IllegalStateException if any required field is unset.
+ */
+ fun build(): MultipartField {
+ val value = checkRequired("value", value)
+ return MultipartField(
+ value,
+ contentType
+ ?: if (
+ value is KnownValue &&
+ (value.value is InputStream || value.value is ByteArray)
+ )
+ "application/octet-stream"
+ else "text/plain; charset=utf-8",
+ filename,
+ )
+ }
+ }
+
+ private val hashCode: Int by lazy { contentHash(value, contentType, filename) }
+
+ override fun hashCode(): Int = hashCode
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) {
+ return true
+ }
+
+ return other is MultipartField<*> &&
+ value == other.value &&
+ contentType == other.contentType &&
+ filename == other.filename
+ }
+
+ override fun toString(): String =
+ "MultipartField{value=$value, contentType=$contentType, filename=$filename}"
+}
diff --git a/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/handlers/EmptyHandler.kt b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/handlers/EmptyHandler.kt
new file mode 100644
index 0000000..3828715
--- /dev/null
+++ b/image-kit-java-core/src/main/kotlin/com/imagekit/api/core/handlers/EmptyHandler.kt
@@ -0,0 +1,12 @@
+@file:JvmName("EmptyHandler")
+
+package com.imagekit.api.core.handlers
+
+import com.imagekit.api.core.http.HttpResponse
+import com.imagekit.api.core.http.HttpResponse.Handler
+
+@JvmSynthetic internal fun emptyHandler(): Handler