diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..2fe58725 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,48 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +## Bug report for Cloudinary Java SDK +Before proceeding, please update to latest version and test if the issue persists + +## Describe the bug in a sentence or two. +… + +## Issue Type (Can be multiple) +[ ] Build - Can’t install or import the SDK +[ ] Performance - Performance issues +[ ] Behaviour - Functions aren’t working as expected (Such as generate URL) +[ ] Documentation - Inconsistency between the docs and behaviour +[ ] Other (Specify) + +## Steps to reproduce +… if applicable + +## Error screenshots or Stack Trace (if applicable) +… + +## Build System +[ ] Maven +[ ] Gradle +[ ] Other (Specify) + +## OS (Please specify version) +[ ] Windows +[ ] Linux +[ ] Mac +[ ] Other (specify) + +## Versions and Libraries (fill in the version numbers) +Cloudinary Java SDK version - 0.0.0 +JVM (dev environment) - 0.0.0 +JVM (production environment) - 0.0.0 +Maven - 0.0.0 / N/A +Gradle - 0.0.0 / N/A + +## Repository +If possible, please provide a link to a reproducible repository that showcases the problem diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..9ac6e086 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +## Feature request for Cloudinary Java SDK +…(If your feature is for other SDKs, please request them there) + + +## Explain your use case +… (A high level explanation of why you need this feature) + +## Describe the problem you’re trying to solve +… (A more technical view of what you’d like to accomplish, and how this feature will help you achieve it) + +## Do you have a proposed solution? +… (yes, no? Please elaborate if needed) diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 00000000..e2509ff5 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,29 @@ +### Brief Summary of Changes + + +#### What does this PR address? +- [ ] GitHub issue (Add reference - #XX) +- [ ] Refactoring +- [ ] New feature +- [ ] Bug fix +- [ ] Adds more tests + +#### Are tests included? +- [ ] Yes +- [ ] No + +#### Reviewer, please note: + + +#### Checklist: + + +- [ ] My code follows the code style of this project. +- [ ] My change requires a change to the documentation. +- [ ] I ran the full test suite before pushing the changes and all the tests pass. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..33920489 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,50 @@ +name: Java SDK Matrix CI + +on: + push: + branches-ignore: + - staging-test + pull_request: + +jobs: + build: + name: Test ${{ matrix.module }} on JDK ${{ matrix.java }} + runs-on: ubuntu-latest + + strategy: + matrix: + java: ['8'] + module: [ 'core', 'http5', 'taglib' ] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK ${{ matrix.java }} + uses: actions/setup-java@v4 + with: + distribution: 'adopt' + java-version: ${{ matrix.java }} + + - name: Clean Gradle plugin cache + run: | + rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + rm -fr $HOME/.gradle/caches/*/plugin-resolution/ + + - name: Cache Gradle + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ matrix.java }}-${{ matrix.module }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Create test subaccount + run: ./gradlew createTestSubAccount -PmoduleName=${{ matrix.module }} + + - name: Load CLOUDINARY_URL and run ciTest + run: | + source tools/cloudinary_url.txt + ./gradlew -DCLOUDINARY_URL=$CLOUDINARY_URL ciTest -p cloudinary-${{ matrix.module }} -i \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea7ec3d7..e732dc7e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,17 +1,49 @@ +## Apple storage files +*.DS_Store + +## Android default ignore +# Built application files +*.apk +*.ap_ + +# Files for the Dalvik VM +*.dex + +# Java class files *.class -# Package Files # -*.jar -*.war -*.ear +# Generated files +bin/ +gen/ + +# Gradle files +.gradle/ +build/ +/*/build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log -.* target/ test-output/ .settings .classpath .project +# intellij +.idea/ *.iml -appengine-web.xml \ No newline at end of file +appengine-web.xml +cloudinary-android/src/androidTest/AndroidManifest.xml + +##Tools +/tools/cloudinary_url.txt +/tools/History.md + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..6cb9303b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,911 @@ +2.3.1 / 2025-08-13 +================== + +* Bump dependencies version + +2.3.0 / 2025-06-18 +================== +* Fix API parameters signature +* Fix build single resource params +* Add skip backup parameter to delete folder api + +2.2.0 / 2025-02-02 +================== + +* Fix Uploader strategy +* Add restore assets by asset ids +* Add allow dynamic list parameter +* Add delete resources by asset ids + +2.1.0 / 2025-01-20 +================== + +* Fix Http client proxy +* Fix Http client system properties support +* Add Cloudinary constructor for `Configuration` +* Fix Register strategy functions + +2.0.0 / 2024-09-29 +================== + +* Bump minimum Java version to 8 +* Secure true by default +* Add `auto_chaptering` and `auto_transcription` to upload API +* New Http client +* Add support for update metadata field set default disabled + +1.39.0 / 2024-07-14 +=================== + +* Add conditional metadata rules api +* Fix rename folder endpoint +* Add config api call +* Add delete backup asset version support +* Add rename folder api support +* Add analyze api +* Add selective response support +* Add access key management +* Add restrictions field to metadata + +1.38.0 / 2024-02-18 +=================== + +* Add `notification_url` support to rename and destroy + +1.37.0 / 2024-01-14 +=================== + +* Update analytics token +* Add missing display name parameter + +1.36.0 / 2023-12-04 +=================== + +* Fix encode url for fetch layer +* Add support to use fetch format + +1.35.0 / 2023-10-11 +=================== + + * Update analytics token + * Add support for `on_success` upload parameter + +1.34.0 / 2023-08-08 +=================== + + * Add visual search support + * Add `toUrl() to Search API + * Add Search folders functionality + * Update Hyper SQL version + * Add support for `media_metadata` parameter + * Add support for `clear_invalid` parameter + +1.33.0 / 2022-09-12 +================== + +* Add dynamic folders support +* Fix VideoTag not appending auth token +* Fix upload with Unicode character not appending a file extension +* Bump springboard version + + +1.32.2 / 2022-05-10 +=================== + + * Fix nexus publishing script + +1.32.1 / 2022-04-25 +=================== + + * Fix double underscore handling during normalization + * Update Spring framework version + +1.32.0 / 2022-04-05 +=================== + +New functionality +----------------- + * Add folder decoupling support + * Support multiple acls in cookies + * Support structured metadata in `resources` api call + * Rename API call returns `metadata` and `context` + * Support start offset and end offset as expression + * Get the details of a single resource by asset_id + * Search by asset id + * Support metadata fields reordering +Other changes +------------- + * Fix `verifySignature` timestamp units + * Fix transformations API call + +1.31.0 / 2022-03-21 +==================== + +New functionality +----------------- + * Get resources by asset id + * Add `enabled` parameter to `updateUser`, `replaceUser` and `createUser` + * Add tags as an array + * Add lowercase support for headers in API responses + * Allow to disable b-frames + * Support download backup version api + * Support `filename_override` upload parameter + * Add support for single character variable + +1.30.0 / 2022-02-02 +=================== + +Other Changes +------------- + * Update `README.md` + * Add feature `SDK analytics` + * Fix a bug where a publicId which contains 'v[num]' is considered to contain a version, therefore the version is skipped. (#242) + +1.29.0 / 2021-02-10 +=================== + +New functionality +----------------- + * Allow setting the user agent (#235) + * Add support for Apache http-client 4.5 (#234) + +Other changes +------------- + * Fix test name in `ExpressionTest` (#233) + + +1.28.1 / 2021-02-03 +================== + + * Fix `api` reuse bug when calling `cloudinary.search()` (#232) + +1.28.0 / 2021-02-01 +================== + + * Add `oauth` support to Admin Api calls. (#230) + * Fix connection reuse when using apache-http-client (versions 4.3 and 4.4) (#231) + +1.27.0 / 2020-11-16 +=================== + +New functionality +----------------- + * Support `type` parameter in `Uploader.updateMetadata()` (#226) + * Add `downloadFolder` method (#219) + * Add eval upload parameter (#217) + * Add support of SHA-256 algorithm in calculation of auth signatures (#215) + * Support different radius for each corner (#212) + * Add support for variables in text style. (#225) + * Add support for 'accessibility_analysis' parameter (#218) + * Support new parameter and modes in `generateSprite()` and `multi()` API cals. + * Add support for `date` param in `Api.usage()` (#210) + + +Other changes +------------- + * Fix named transformation with spaces (#224) + * Fix normalize_expression for complex cases (#216) + * Detect data URLs with suffix in mime type (#213) + +1.26.0 / 2020-05-05 +=================== + +New functionality +----------------- + + * Add variable support to `Transformation.opacity()` (#209) + * Add support for restoring deleted datasource entries (#207) + * Add support for 32 char SHA-256 URL signatures. + * Add support for `pow` operator in expressions (#198) + * Add signature checking methods (#193) + +Other changes +------------- + + * Fix handling of `max_results` and `next_cursor` parameters for folders api (#203) + * Fix `normalize_expression` when a keyword is used in a variable name (#205) + +1.25.0 / 2020-02-06 +=================== + +New functionality +----------------- + * Allow generating archive with multiple resource types (#174) + * Add validation for `CLOUDINARY_URL` scheme (#185) + * Support create folder API (#188) + +Other changes +------------- + * Fix/provisioning api params (#195) + * Encode URLs in API calls (#186) + * Improve support for modifying `set` type metadata fields. (#194) + * Ignore `URL` in AuthToken generation if `ACL` is provided (#184) + +1.24.0 / 2019-09-12 +=================== + + * Add support for `cinemagraph_analysis` parameter. (#182) + * Rename Account API methods, add convenience overloads. (#181) + +1.23.0 / 2019-08-15 +=================== + +New functionality +----------------- + * Add account API support (user and cloud management) (#176) + * Add structured metadata APIs and entities (#171) + * Add duration to conditions in video (#172) + * Add support for `live` parameter to Upload Preset (#173) + * Add support for folder deletion (#170) + * Add support for forcing a version when generating URLs. + * Add support for custom pre-functions in transformation (wasm/remote). (#162) + +Other changes +------------- + * Fix base64 url validation (accept parameters). (#165) + * Fix build script and travis.yml to support more java versions. + * Remove test for similarity search (#163) + +1.22.1 / 2019-02-13 +=================== + + * Fix Java 1.6 support (#161) + * Fix eager transformation chaining. (#159) + +1.22.0 / 2019-01-22 +=================== + + * Add JVM version to user agent (#157) + * Add support for range value in `Transformation.fps()` (#155) + * Add support for google-storage URLs (`gs://`) in uploads (#154) + * Add `quality_analysis` param in upload, explicit and api.resource calls + * Add `named` parameter to list-transformations api. + +1.21.0 / 2018-11-05 +=================== + +New functionality +----------------- + * Add support for font antialiasing and font hinting for text overlays + +Other changes +------------- + * Clone configuration in `Url.clone()` + +1.20.0 / 2018-10-10 +=================== + +New functionality +----------------- + + * Add support for web assembly and lambda functions in transformations + +Other changes +------------- + + * Improve performance of `url.generate()` method. + * Fix url encoding for AuthToken generation + +1.19.0 / 2018-07-22 +=================== + +New functionality +----------------- + + * Add support of `auto` value for `start_offset` transformation parameter + * Feature/keyframe interval support + +Other changes +------------- + + * Fix content range header in chunked upload (force US locale) + * Keep original filename in `uploadLarge` before sending the InputStream + * Update gradle for java 7 TLS fix (https://github.com/gradle/gradle/issues/5740) + * Fix Api list tags test - verify the list instead of specific tags + * Cleanup upload preset from `testGetUploadPreset` (#129) + * Add int overload to `TextLayer.letterSpacing()` + * Fix responsive breakpoint format field implementation + * Separate modules to run on different travis jobs. + * Remove `test02Resources` test (broken and unnecessary). + * Fix raw convert error message test + +1.18.0 / 2018-03-15 +=================== + +New functionality +----------------- + + * Add access control parameter to upload and update calls + +Other changes +------------- + + * Fix authToken generation when using acl + * Fix `testOcrUpdate()` test case (#119) + * Configure .travis.yml to show more test information. + * Verify `testDeleteByToken` takes all original config into account (#116) + * Replace `pom.xml` link with `build.gradle` in README.md + +1.17.0 / 2017-11-26 +=================== + + * Add missing params to `explicit` and `upload` api + * Remove Android from Travis configuration and add JDK versions + +1.16.0 / 2017-09-26 +=================== + + * Change url suffix and root path limitations + * Add update_version.sh + * Update Readme to point to HTTPS URLs of cloudinary.com + * Fix android repository link + +1.15.0 / 2017-09-05 +=================== + +New functionality +----------------- + + * Add format field to `ResponsiveBreakpoint`. + * The Android SDK has been moved to https://github.com/cloudinary/cloudinary_android + +Other changes +------------- + + * Add badges to README + * Create LICENSE + * Centralize response handling and respect `returnError` param. + * Fix boolean config values in tag generation + * Fix project for java8, update cloudinary dependencies. + +1.14.0 / 2017-07-20 +=================== + +New functionality +----------------- + + * Add support for uploading remote urls through `Uploader.uploadLarge()` + * Support resuming `uploadLarge` + * Streaming profile support. + * Update TravisCI to explicitly set distribution + * Allow deleteByToken to pass through when there's no api secret in config. + +Other changes +------------- + + * Add test for listing transformations with cursor. + * Merge branch 'master' into patch-1 + * Make restore test run in parallel + * Set javadoc encoding to UTF-8. + * Update gradle to 4.0.1. + * Fix test to run in parallel. + * Remove use of `DatatypeConverter` which is not supported in Android + * Update Cloudinary dependencies version for java sample project. + * Merge pull request #84 from elevenfive/master + * Close responsestream + * Merge pull request #83 from theel0ja/patch-1 + * Improved formatting of markdown + +1.13.0 / 2017-06-12 +=================== + +New functionality +----------------- + + * Add support for `format` in Responsive breakpoints transformation. (#78) + * Add `url_suffix` support for private images. (#76) + * Add `type` parameter to `Api.publishResource()` (#73) + * Add support for fetch overlay/underlay (#69) + * Add `deleteByToken` to `Uploader`. + * Rename `deleteDerivedResourcesByTransformations` to `deleteDerivedByTransformation` + * Fix `deleteStreamProfile` no-options overload. + * Add support for *TravisCI* tests + * Replace Maven with Gradle as build tool + +Other changes +------------- + + * Parallelize tests. + +1.12.0 / 2017-05-01 +=================== + +New functionality +----------------- + + * Add Search API + +1.11.0 / 2017-04-25 +=================== + +New functionality +----------------- + + * Add `fps` transformation parameter. + +1.10.0 / 2017-04-02 +=================== + +New functionality +----------------- + + * Add upload progress callback for Android + * Add support for notification_url param in `Api.update` + * Add support for `allowMissing` parameter in archive creation. + * Add `videoTag(String source)` overload to `Url`. + * Add `deleteDerivedResourcesByTransformations` to Admin Api + * Add `ocr` to explicit API + +Other changes +------------- + + * Add `ocr` gravity value tests + * Fix ParseException when accessing `Response.rateLimits` with default locale set to non-english. + * Add javaDoc for `MultipartUtility.close()` + * Merge pull request #46 Close streams in UploaderStrategy + * Add javaDoc for `MultipartCallback` + * Add `progressCallback` test cases. + * Add test for `generate_archive` of raw resources. + * Fix `MultipartUtility` - Verify inner stream is closed if an exception is thrown somewhere along the way. + +1.9.1 / 2017-03-14 +================== + + * Add expires at to generate archive (#68) + * Add `skip_transformation_name` parameter to generate archive. (#67) + * Fix variables. + * Fix variable regex. + * Make Expression.serialize return normalized expression + * Fix variable sorting. + * Add tests for variable order. + * Avoid normalizing negative numbers. + * Normalize effect parameter + * Remove duplicate quality parameter line. + +1.9.0 / 2017-03-08 +================== + +New functionality +----------------- + + * Support **User defined variables** and **expressions**. + * Add `async` parameter to upload params(#63) + * Add `expired_at` parameter to private download. (#60) + * Add `moderation` parameter in explicit call (#59) + +Other changes +------------- + + * Fix double encoding for commas and slashes in text layers (#66) + * Add artistic filter test (#65) + * Add gravity-auto test (#64) + * Fix `OutOfMemoryError` when uploading large files in android. Fixes #55 (#57) + * Fix encoding error in api update resource (#61) + +1.8.1 / 2017-02-22 +================== + + * Add support for URL authorization token. + * Refactor AuthToken. + * Refactor tests for stability + * Support nested objects in CLOUDINARY_URL. e.g. foo[bar]=100. + * Add maven items to `.gitignore`. + +1.8.0 / 2017-02-08 +================== + +New functionality +----------------- + + * Access mode API + +Other changes +------------- + + * Fix listing direction test. + * Refactor `multi` test + +1.7.0 / 2017-01-30 +================== + +New functionality +----------------- + + * Add Akamai token generator + +Other changes +------------- + + * Fix "multi" test + +1.6.0 / 2017-01-08 +================== + + * Add Search by context API + +1.5.0 / 2016-11-19 +================== + +New functionality +----------------- + + * Add context API + * Escape `\` and `=` in context + * Add `removeAllTags` API + +1.4.6 / 2016-10-27 +==================================== + + * Add streaming profiles API + +1.4.5 / 2016-09-16 +==================================== + * Better handling of missing/unreadable local files + +1.4.4 / 2016-09-15 +==================================== + * Fix issue when uploading URL with \n + +1.4.3 / 2016-09-09 +==================================== + +New functionality +----------------- + + * New Admin API `Publish`. + * Support `to_type` in `rename`. + * Add `skip_transformation_name` and `expires_at` to archive parameters. + * Support Client Hints. + +Other changes +------------- + + * Add deprecation message to `Layer` classes. + * Define `MockableTest` + * Add static import of `asMap` and `emptyMap`. Suppress deprecation warnings for backward compatibility tests. + * Refactor Quality and Width tests. + * Update Junit version and add JUnitParams. + * Add Hamcrest tests. + * Add tests for auto width and original width and height ( `ow`, `oh`) values + * Add `timeout`, `connect_timeout` and `connection_request_timeout` to HTTP43 and HTTP44 Api. + +1.4.2 / 2016-05-16 +==================================== + + * Sent params as entities for PUT, POST + * Use "_method" with "delete" instead of HttpDelete. + * Add `next_cursor` to `Api#transformation()` + * Update Google App Engine demo + * Add script to create unsigned upload preset for the Android test + * Use dynamic tag in sprites test + * Add SDK_TEST_TAG to all resources being created. + * Remove API limits test + +1.4.1 / 2016-03-23 +==================================== + * Rename conditional parameters `faces` and `pages` to `faceCount` and `pageCount` + + +1.4.0 / 2016-03-19 +==================================== + * Add Condition builder for faces + * Modify explicit test - don't use twitter + * Modify categorization test result value + * Add Conditional Transformations + * Cleanup Whitespace + * Use variables for public_id's in rename tests + * Fix uploadLarge to use X-Unique-Upload-Id instead of updating params. Solves #18 + * Fix support for non-ascii chars in upload URL + +1.3.0 / 2016-01-19 +==================================== + + * Add `responsive_breakpoints` paramater + * Use `TextLayer` instead of `TextLayerBuilder`. Use `getThis()` instead of `self()`. + * Use constant and meaningful name for upload preset. Rearrange imports. + * Update SDK versions in Android projects. + * Support cloudinary credentials URL that has an API_KEY but no API_SECRET + * Remove redundant `deleteConflictingFiles`. + * Merge branch 'master' of github.com:cloudinary/cloudinary_java + * support createArchive + * line spacng support in text overlay + * Create separate test class for Layer + * Rename Layer classes. Rename self() to getThis() to match the pattern. + * Merge branch 'master' of github.com:cloudinary/cloudinary_java + * change user agent - remove spaces. stricter layer parameter check. fix underlay method signature + * Merge branch 'master' of github.com:cloudinary/cloudinary_java + * Fix Android complex filename test + +cloudinary-parent-1.2.2 / 2016-01-19 +==================================== + + * Fix Android tests + * Enable apache http 4.3 strategy + * Support easy overlay/underlay construction + * Support upload mappings api. add missing restore test + * Support the restore api + * Normalize user agent + * Add invalidate flag to rename and explicit + * Support aspect ratio transformation param + * Add filename and complex filename test + * Fix encoding issues when JVM default encoding is not UTF-8 + * Revent timeout exception change + * Support filename in upload options. close response objects in http44 + * Update README. Fixes #28 + * Merge pull request #26 from wagaun/master + * Fixing typo on exception + * Update README.md + +cloudinary-parent-1.2.1 / 2015-06-18 +==================================== + + * Disable java8 doclint + * Fix references to 1.1.4-SNAPSHOT. Fix wrong URLs in README.md + * Fix documentation and imports + * Modify exception message to say that Admin API is not supported. + * Fix HTML escaping (fixes upload tags) + * Allow android unsigned upload without api_key + * Fix http44 response closing. + +cloudinary-parent-1.2.2 / 2015-10-11 +==================================== + + * Support apache http 4.3 strategy + * Support easy overlay/underlay construction + * Support upload mappings api + * Support the restore api + * Normalize user agent + * Add invalidate flag to rename and explicit + * Support aspect ratio transformation param + * Fix encoding issues when JVM default encoding is not UTF-8 + * Support filename in upload options + * Close response objects in http44. + +cloudinary-parent-1.2.0 / 2015-04-13 +==================================== + + * Support httpcomponents 4.4 + * Support for video tag and transformations + * Add video transformation parameters and zoom transformation + * Support ftp url upload + * Support eager_async in explicit + * Fix UTF-8 issues in API + * Add support for video tag. refactor Url based tags + * Scrub UrlBuilderStrategy + * Enable crippled core mode without loading strategies + * Move core test to core + * Use URLEncoder instead of AbstractUrlBuilderStrategy. + * Use upload_chuncked endpoint for upload large + * Improved parameter support for upload_large. + * support byte[] file input for upload + +cloudinary-parent-1.1.3 / 2015-02-24 +==================================== + + * Fix test after file name change + * Added timeout parameter to admin api and Fixed test and configuration issues + +cloudinary-parent-1.1.2 / 2015-01-15 +==================================== + + * Fix support for string eager parameters e.g. for safe mobile flow + * Merge pull request #17 from cloudinary/eager_upload_params + * merged android signature fix + * eager upload params can be both string or List + +cloudinary-parent-1.1.1 / 2014-12-22 +==================================== + + * Support secure domain sharding + * Don't sign version component + * Support url suffix and use root path + * renamed urlSuffix to suffix + * Support tags in upload large. + * Change log and version update + * added new options to url tag + * added invalidate to bulk deletes + * Add missing tests in adnroid-test. fix signing tests in android-test. be more specific with exception class in http42 Cloudinary tests. + * updated Url.generate method (b4 tests) + * bug fixes + +cloudinary-parent-1.1.0 / 2014-11-18 +==================================== + + * Merge branch 'globalize' of github.com:codeinvain/cloudinary_java + * Remove redundant depndencies + * - changed org.json to org.cloudinary.json due to Android optimization issues . - removed dependency on SimpleJSON from tablib + * Update CHANGES.txt + * Merge branch 'globalize' + * Fix documentation. Fix dependencies + * Fix modules artifactId + * promoted minor version (1.0.x -> 1.1.x) & fixed documentation external links + * added deprecated asMap method to Cloudinary (support old api) + * updated documentation , fixed sample projects + * promoted minor version (1.0.x -> 1.1.x) & fixed documentation external links + * added deprecated asMap method to Cloudinary (support old api) + * updated documentation , fixed sample projects + * add support for signed urls in tag helpers (image and url) + * Git ignore cloudinary-android-test/src/main/AndroidManifest.xml. Fix tag lib dependency + * Remove httpclient dependencies from cloudinary-core. Use main version in both http42 and android versions. Remove getRawResponse from ApiResponse + * Merge branch 'globalize' of github.com:codeinvain/cloudinary_java + * merged config & builder + * Update README.md + * cloudinary credentials removed + * http42 + android tests pass + * changed architecture to core + strategies + * removed shared classes + * android jar + * maven build , project dependency core -> http42 -> taglib + * unified Java API and created basic implementation + * custom StringUtils + * support folder listing API + +cloudinary-parent-1.0.14 / 2014-07-29 +===================================== + + * Add background_removal + * Support return_delete_token in upload/update params + * Support responsive and hidpi + * Support custom coordinates. + +cloudinary-parent-1.0.13 / 2014-04-29 +===================================== + + * Add support for opacity + * Support upload_presets + * Support unsigned uploads + * Support start_at for resource listing + * Support phash for upload and resource details + * Support rate limit header in Api calls + * Initial commit Google App engine sample + * Merge remote master + * Allow passing ClientConnectionManager + +cloudinary-parent-1.0.12 / 2014-03-04 +===================================== + + * Increment version to 1.0.12 + * Fix uploader API calls handling of non-string parameters e.g. Booleans + +cloudinary-parent-1.0.11 / 2014-03-04 +===================================== + + * Document releases in CHANGES.txt + * Fix test - raw upload parts must be > 5m + * better large raw upload support + * Merge branch 'master' of https://github.com/cloudinary/cloudinary_java + * new update method + * Add listing by moderation kind and status + * Add moderation status in listing + * Add moderation flag in upload + * Add moderation_status in update + * Add ocr, raw_conversion, categorization, detection, similarity_search and auto_tagging parameters in update and upload + * Add support for uploading large raw files + +cloudinary-parent-1.0.10 / 2014-01-27 +===================================== + + * add discard_original_filename upload flag. Formatting in tests + * support setting context in explicit + * Add direction support to resource listing. + +cloudinary-parent-1.0.9 / 2014-01-10 +==================================== + + * remove delete_all from tests. fix face coordinates in explicit + * Merge branch 'master' of https://github.com/cloudinary/cloudinary_java + * add user agent. fix api test + * refactor Map encoding for upload + * Merge branch 'signedurl' + * Update README.md + * Merge branch 'master' of https://github.com/cloudinary/cloudinary_java + * support multiple face coordinates in upload and explicit. optionaly use Coordinates as a wrapper of multiple rectangles + * add support for overwrite in taglib + * add support for overwrite boolean in upload + * support signed urls + * delete all + cursors, tag and context flags in lists, list by public ids, add support in upload for: face_coordinates, alowed_formats, context + * change dependency to published 1.0.8 and change installation instructions accordingly + +cloudinary-parent-1.0.8 / 2013-12-20 +==================================== + + * Fix implementation of SmartUrlEncoder in case of non-ascii characters + * fix callback when servlet is not at root + * better handling of raw files + * add subsections in README. Add this to memeber assignments + * move most of stored file logic to core. support stored file in url and url and image tags. add a readme to the sample project + * add support for named transformations as tag attribute + * add support for local secure (and implicit from request) and cdn_subdomain + * cleanup and upload parameters completeness + * change images to use inline transformations when possible. fix image link in list + * fix inline transformation in image. add inline trnaformation in url + * initial commit of photo album sample. added additional or modified existing tag helpers to taglib to enable more robust transformations and to allow cloufdinary URLs outside of images and to allow specifying images from facebook/twitter and support jQuery direct upload. + +cloudinary-parent-1.0.7 / 2013-11-02 +==================================== + + * Support the color parameter + * Merge branch 'master' of https://github.com/cloudinary/cloudinary_java + * add support for unique_filename and added a test for use_filename + * Merge pull request #9 from AssuredLabor/transformationAttr + * add transformation attribute to cloudinary upload tag + * Fix handling of boolean parameters on upload + +cloudinary-parent-1.0.6 / 2013-08-07 +==================================== + + * Rename prepareUploadTagParams to uploadTagParams + * Escape all public_ids including non-http ones. + * Merge pull request #7 from AssuredLabor/extractUploadParams + * Updated so we don't escapeHTML unless necessary for the server side. This allows the client-side to receive a JS hash / object directly. This is useful, depending on how the input is rendered. + * Extracted upload tagParams and upload url functionality into their functions, this will facilitate frameworks like Angular fetching the server-side params + +cloudinary-parent-1.0.5 / 2013-07-31 +==================================== + + * Support folder and proxy upload parameters + * Fix string comparison of secureDistribution + * Change secure urls to use *res.cloudinary.com + * Support Admin API ping + * Support generateSpriteCss + +cloudinary-parent-1.0.4 / 2013-07-15 +==================================== + + * Issue #6 - add instructions on using as a maven dependency + * Support raw data URI + * Support zipDownload. Cleanup signing code + * Support s3 and data:uri urls + +cloudinary-parent-1.0.3 / 2013-06-04 +==================================== + + * Cleanup pom.xml, Fix imageUploadTag test, Fix imports + * Introduced a new image tag for jsps, you can use it like this: + * don't track eclipse resources + * Add the callback and the signature to the image tag + * In the tag lib, use the Uploader's tag generator * Allow null file parameters + * enhancements to the HTML processing + * cleaned up the tag rendering. There is some more flexibility that needs to be added to the tag, but it looks like the core of it is working ok. + * correctly located the cloudinary tld and updated to use the new classname of the tag Added a singleton manager to ease spring support. + * renamed tag to make more sense + * First pass at an upload tag and support code + * Refactored Cloudinary Java into multiple modules without breaking the module naming convention already established. * Created a -taglib module to support constructing file input tags on the server side, since it requires some server side API signing. * Separate modules allow users who are writing stand-alone applications (not depending on the Servlet API) not to have a dependency on it. + * Fixing code sample, referencing Android + +cloudinary-1.0.2 / 2013-04-08 +============================= + + * Upgrade version to 1.0.2-SNAPSHOT + * Don't fail api tests if api_secret is not given + * Don't fail api tests if api_secret is not given + * pom fixes + * Preparation for Maven repository submission + * Merge Maven preperation by shakiba + * Missing file for rename test + * Invalidate flags in upload and destroy + * Private download link generator + * Support for short urls for image/upload + * Support for folders + * Support rename + * Support unsafe transformation update + * Fix tags api support of multiple public ids + * ready for maven central + * Fixing URLs in readme + * Support akamai + * Support for sprite genreation, multi and explode. Support new async/notification flags + * Merge git://github.com/andershedstrom/cloudinary_java + * Support for usage API call + * Support image_metadata flag in upload and API + * Update README.md + * fixed regexp bug, regexp didn't work + * Updated pom.xml to handle custom src and test-src directories + * Allow giving pages flag to resource details API + * Fix check for limit. Fix htmlWidth visibility + * Support for info flags in upload + * Support for transformation flags + * Support deleteResourcesByTag Support keep_original in resource deletion + * Uploader.imageUploadTag - helper for create input tag for direct upload to Cloudinary via JS + * Added README + * Java naming conventions. Map utility methods + * Initial commit diff --git a/CHANGES.txt b/CHANGES.txt deleted file mode 100644 index 29c03669..00000000 --- a/CHANGES.txt +++ /dev/null @@ -1,2 +0,0 @@ -1.0.11 - 2014-03-04 - new update method. add listing by moderation kind and status. add moderation status in listing. add moderation flag in upload. add moderation_status in update. add ocr, raw_conversion, categorization, detection, similarity_search and auto_tagging parameters in update and upload. add support for uploading large raw files -1.0.12 - 2014-03-04 - Fix handling of Booleans in uploader API diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..8cace2e7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Cloudinary + +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. diff --git a/MAVEN_CENTRAL_PUBLISHING_GUIDE.md b/MAVEN_CENTRAL_PUBLISHING_GUIDE.md new file mode 100644 index 00000000..6fb96006 --- /dev/null +++ b/MAVEN_CENTRAL_PUBLISHING_GUIDE.md @@ -0,0 +1,437 @@ +# Maven Central Publishing Guide - Cloudinary Java SDK + +This guide documents the complete process for publishing the Cloudinary Java SDK to Maven Central using the new Central Portal (central.sonatype.com), replacing the deprecated OSSRH system. + +## 🎯 **Overview** + +- **Old System:** `oss.sonatype.org` (dead, returns 401 errors) +- **New System:** `central.sonatype.com` with manual bundle upload +- **Method:** Manual bundle creation and upload (not automated plugin publishing) +- **Requirements:** Complete artifacts with checksums and GPG signatures +- **Current Version:** 2.3.1 → Next version (e.g., 2.3.2) + +## 📋 **Prerequisites** + +1. **Credentials:** + - `centralUsername` and `centralPassword` for central.sonatype.com + - Legacy `ossrhToken` and `ossrhTokenPassword` (if available) + +2. **GPG Setup:** + - GPG key imported: `6B42474E50D0D89A01B40AC225FE63F85DCB788F` + - Private key available in repository: `private-key.asc` + - Password: `nwov0aaStnO4` + +3. **Java Version:** + - **Java 8+** (current project targets Java 8) + - Verify with: `java -version` + +## 🔧 **Configuration Changes Required** + +### 1. Update Root `build.gradle` + +```gradle +plugins { + id 'maven-publish' + // Remove the old nexus plugin: id 'io.github.gradle-nexus.publish-plugin' version '1.0.0' +} + +allprojects { + repositories { + mavenCentral() + } + project.ext.set("publishGroupId", group) +} + +// Remove the old nexusPublishing block - we'll create bundles manually for Central Portal + +tasks.create('createTestSubAccount') { + doFirst { + println("Task createTestSubAccount called with module $moduleName") + def cloudinaryUrl = "" + + // core does not use test clouds, skip (keep empty file for a more readable generic travis test script) + if (moduleName != "core") { + println "Creating test cloud..." + def baseUrl = new URL('https://sub-account-testing.cloudinary.com/create_sub_account') + def connection = baseUrl.openConnection() + connection.with { + doOutput = true + requestMethod = 'POST' + def json = new JsonSlurper().parseText(content.text) + def cloud = json["payload"]["cloudName"] + def key = json["payload"]["cloudApiKey"] + def secret = json["payload"]["cloudApiSecret"] + cloudinaryUrl = "CLOUDINARY_URL=cloudinary://$key:$secret@$cloud" + } + } + + def dir = new File("${projectDir.path}${File.separator}tools") + dir.mkdir() + def file = new File(dir, "cloudinary_url.txt") + file.createNewFile() + file.text = cloudinaryUrl + + println("Test sub-account created successfully!") + } +} +``` + +### 2. Create New `publish.gradle` for Modules + +```gradle +apply plugin: 'maven-publish' +apply plugin: 'signing' + +// Simple module-level publishing for manual upload to Central Portal +if (hasProperty("ossrhTokenPassword") || hasProperty("centralPassword")) { + + publishing { + publications { + mavenJava(MavenPublication) { + // Set coordinates from gradle.properties + groupId = project.ext.publishGroupId + artifactId = project.name + version = project.version + + // Include JAR artifacts and components for Java + from components.java + artifact sourcesJar + artifact javadocJar + + pom { + name = getModuleName(project.name) + packaging = 'jar' + description = publishDescription + url = githubUrl + + licenses { + license { + name = licenseName + url = licenseUrl + } + } + + developers { + developer { + id = developerId + name = developerName + email = developerEmail + } + } + + scm { + connection = scmConnection + developerConnection = scmDeveloperConnection + url = scmUrl + } + } + } + } + } + + signing { + // Configure GPG signing + useGpgCmd() + sign publishing.publications.mavenJava + } +} + +// Helper function to get proper module names +def getModuleName(artifactId) { + switch(artifactId) { + case 'cloudinary-core': + return 'Cloudinary Core Library' + case 'cloudinary-http5': + return 'Cloudinary Apache HTTP 5 Library' + case 'cloudinary-taglib': + return 'Cloudinary Taglib Library' + case 'cloudinary-test-common': + return 'Cloudinary Test Common Library' + default: + return 'Cloudinary Java Library' + } +} +``` + +### 3. Update Module `build.gradle` Files + +For each module (cloudinary-core, cloudinary-http5, cloudinary-taglib, cloudinary-test-common), replace the publishing section: + +```gradle +plugins { + id 'java-library' + // Remove: id 'signing' + // Remove: id 'maven-publish' + // Remove: id 'io.codearte.nexus-staging' version '0.21.1' +} + +apply from: "../java_shared.gradle" +apply from: "../publish.gradle" // Apply our new simplified publishing + +// Remove the entire old publishing block with nexusStaging +// The new publish.gradle handles everything +``` + +### 4. Update `gradle.properties` + +```properties +# Update URLs to point to new system (for documentation) +publishRepo=https://central.sonatype.com/ +snapshotRepo=https://central.sonatype.com/ +publishDescription=Cloudinary is a cloud service that offers a solution to a web application's entire image management pipeline. Upload images to the cloud. Automatically perform smart image resizing, cropping and conversion without installing any complex software. Integrate Facebook or Twitter profile image extraction in a snap, in any dimension and style to match your website's graphics requirements. Images are seamlessly delivered through a fast CDN, and much much more. This Java library allows to easily integrate with Cloudinary in Java applications. +githubUrl=http://github.com/cloudinary/cloudinary_java +scmConnection=scm:git:git://github.com/cloudinary/cloudinary_java.git +scmDeveloperConnection=scm:git:git@github.com:cloudinary/cloudinary_java.git +scmUrl=http://github.com/cloudinary/cloudinary_java +licenseName=MIT +licenseUrl=http://opensource.org/licenses/MIT +developerId=cloudinary +developerName=Cloudinary +developerEmail=info@cloudinary.com + +# Update version for next release +group=com.cloudinary +version=2.3.2 + +gnsp.disableApplyOnlyOnRootProjectEnforcement=true + +# see https://github.com/gradle/gradle/issues/11308 +systemProp.org.gradle.internal.publish.checksums.insecure=true +``` + +## 🚀 **Step-by-Step Publishing Process** + +### Step 1: Environment Setup + +```bash +# Navigate to project +cd /Users/adimizrahi/Development/Java/cloudinary_java + +# Verify Java version (should be Java 8+) +java -version +javac -version + +# Set GPG environment for batch signing +export GPG_TTY=$(tty) +``` + +### Step 2: Clean and Build All Artifacts + +```bash +# Clean previous builds and generate all artifacts +./gradlew clean publishToMavenLocal +``` + +**Expected Output:** +- JAR files for each module (cloudinary-core, cloudinary-http5, cloudinary-taglib, cloudinary-test-common) +- Sources JARs (`-sources.jar`) +- Javadoc JARs (`-javadoc.jar`) +- POM files with correct XML structure +- All artifacts signed with GPG (`.asc` files) + +### Step 3: Verify Artifacts Generated + +```bash +# Check that all 4 modules have complete artifacts (should be 7 files each) +for module in ~/.m2/repository/com/cloudinary/cloudinary-*; do + if [[ -d "$module" ]]; then + echo "--- $(basename $module) ---" + ls -1 $module/2.3.2/ 2>/dev/null | grep -E "\.(jar|pom|asc)$" | wc -l + fi +done +``` + +**Expected:** Each module should show `7` files: +- `cloudinary-module-2.3.2.jar` + `.asc` +- `cloudinary-module-2.3.2-sources.jar` + `.asc` +- `cloudinary-module-2.3.2-javadoc.jar` + `.asc` +- `cloudinary-module-2.3.2.pom` + `.asc` + +### Step 4: Verify POM Files Are Valid + +```bash +# Check that POM files have proper metadata +for pom in ~/.m2/repository/com/cloudinary/cloudinary-*/2.3.2/*.pom; do + if [[ -f "$pom" ]]; then + echo "--- $(basename $pom) ---" + echo "Name tags: $(grep -c "" "$pom")" + echo "Description: $(grep -c "" "$pom")" + echo "License: $(grep -c "" "$pom")" + echo "Developer: $(grep -c "" "$pom")" + echo "SCM: $(grep -c "" "$pom")" + fi +done +``` + +**Expected:** Each POM should have all required metadata elements. + +### Step 5: Generate Additional Checksums + +```bash +cd ~/.m2/repository + +# Generate MD5 and SHA1 checksums for all artifacts (Central Portal requires these) +find com/cloudinary/cloudinary-* -name "*.jar" -o -name "*.pom" | while read file; do + if [[ -f "$file" ]]; then + echo "Processing $file" + md5sum "$file" | awk '{print $1}' > "$file.md5" + sha1sum "$file" | awk '{print $1}' > "$file.sha1" + fi +done +``` + +### Step 6: Verify Complete File Set + +```bash +cd ~/.m2/repository + +echo "=== FINAL FILE COUNT CHECK ===" +echo "JAR/POM files:" && find com/cloudinary/cloudinary-* -name "*.jar" -o -name "*.pom" | wc -l +echo "GPG signatures:" && find com/cloudinary/cloudinary-* -name "*.asc" | wc -l +echo "MD5 checksums:" && find com/cloudinary/cloudinary-* -name "*.md5" | wc -l +echo "SHA1 checksums:" && find com/cloudinary/cloudinary-* -name "*.sha1" | wc -l +``` + +**Expected File Count:** +- 4 modules × 4 artifacts each = **16 original files** +- **16 GPG signatures** (`.asc`) +- **16 MD5 checksums** (`.md5`) +- **16 SHA1 checksums** (`.sha1`) +- **Total: 64 files** + +### Step 7: Create Final Bundle + +```bash +cd ~/.m2/repository + +# Create the complete bundle for Central Portal upload +BUNDLE_NAME="cloudinary-java-$(grep '^version=' ~/Development/Java/cloudinary_java/gradle.properties | cut -d'=' -f2)-bundle-COMPLETE.tar.gz" + +tar -czf ~/"$BUNDLE_NAME" \ +$(find com/cloudinary/cloudinary-* \ + -name "*.pom" -o -name "*.jar" \ + -o -name "*.md5" -o -name "*.sha1" -o -name "*.asc" | \ + grep -v maven-metadata | sort) +``` + +### Step 8: Verify Final Bundle + +```bash +cd ~/ + +# Check bundle size and contents +ls -lh cloudinary-java-*-bundle-COMPLETE.tar.gz +echo "--- File count ---" +tar -tzf cloudinary-java-*-bundle-COMPLETE.tar.gz | wc -l +echo "--- Sample contents ---" +tar -tzf cloudinary-java-*-bundle-COMPLETE.tar.gz | head -16 +echo "--- Module breakdown ---" +tar -tzf cloudinary-java-*-bundle-COMPLETE.tar.gz | grep -E "(core|http5|taglib|test-common)" | cut -d'/' -f3 | sort | uniq -c +``` + +**Expected:** +- **Size:** ~1-2MB (smaller than Android due to fewer dependencies) +- **Files:** 64 total +- **Modules:** 4 modules with 16 files each +- **Contents:** Each module should have JARs, POMs, and all checksums/signatures + +## 📤 **Upload to Central Portal** + +### Manual Upload Process + +1. **Login:** Go to https://central.sonatype.com/ +2. **Credentials:** Use `centralUsername` and `centralPassword` +3. **Upload:** Navigate to "Upload Component" or "Publish" +4. **Bundle:** Select the `.tar.gz` file created in Step 7 +5. **Publishing Type:** Choose "USER_MANAGED" +6. **Publication Name:** "Cloudinary Java SDK v{version}" + +### Expected Validation + +The Central Portal will validate: +- ✅ **POM structure** (proper XML with required metadata) +- ✅ **Artifact integrity** (MD5/SHA1 checksums match) +- ✅ **Signatures** (GPG signatures valid) +- ✅ **Completeness** (all required files present) +- ✅ **Java compatibility** (JAR files are valid) + +## 🛠 **Troubleshooting** + +### Common Issues & Solutions + +1. **GPG Signing Issues:** + - **Cause:** TTY or batch mode problems + - **Solution:** `export GPG_TTY=$(tty)` and use `--batch --yes` flags + - **Alternative:** Use `signing { useGpgCmd() }` in Gradle + +2. **Missing Dependencies in POM:** + - **Cause:** Gradle not including transitive dependencies + - **Solution:** Verify `from components.java` includes dependencies + - **Check:** Examine generated POM files for `` section + +3. **Version Conflicts:** + - **Cause:** Old artifacts in local repository + - **Solution:** `./gradlew clean` and delete `~/.m2/repository/com/cloudinary/` + +4. **Module Configuration Issues:** + - **Cause:** Inconsistent `build.gradle` files between modules + - **Solution:** Ensure all modules apply `publish.gradle` consistently + +5. **Bundle Upload Failures:** + - **Cause:** Missing or corrupted files in bundle + - **Solution:** Verify all 64 files present and re-create bundle + +## 📋 **Module-Specific Information** + +### Cloudinary Core (`cloudinary-core`) +- **Artifact ID:** `cloudinary-core` +- **Description:** Core Cloudinary functionality +- **Dependencies:** Minimal (mostly standard Java libraries) + +### Cloudinary HTTP5 (`cloudinary-http5`) +- **Artifact ID:** `cloudinary-http5` +- **Description:** Apache HTTP Client 5 implementation +- **Dependencies:** `cloudinary-core`, Apache HTTP Components + +### Cloudinary Taglib (`cloudinary-taglib`) +- **Artifact ID:** `cloudinary-taglib` +- **Description:** JSP Taglib for Cloudinary +- **Dependencies:** `cloudinary-core`, Servlet API + +### Cloudinary Test Common (`cloudinary-test-common`) +- **Artifact ID:** `cloudinary-test-common` +- **Description:** Shared test utilities +- **Dependencies:** `cloudinary-core`, JUnit, test frameworks + +## 📝 **Version Update Checklist** + +For publishing a new version: + +- [ ] Update `version` in `gradle.properties` +- [ ] Update this guide with new version number +- [ ] Run complete publishing process (Steps 1-8) +- [ ] Verify all 64 files in final bundle (4 modules × 16 files) +- [ ] Upload to Central Portal +- [ ] Verify publication appears on Maven Central +- [ ] Update GitHub releases and tags +- [ ] Test artifacts can be consumed by dependent projects + +## 🔗 **References** + +- **Central Portal:** https://central.sonatype.com/ +- **Migration Guide:** https://central.sonatype.org/publish/publish-guide/ +- **Gradle Publishing:** https://docs.gradle.org/current/userguide/publishing_maven.html + +--- + +**Last Updated:** [Current Date] +**Tested Version:** 2.3.2 +**Success Rate:** ✅ To be tested with this process + +## 🚨 **Key Differences from Android SDK** + +1. **No AAR files** - Uses JAR files instead +2. **Java components** - Uses `components.java` instead of `components.release` +3. **Simpler setup** - No Android-specific build tools required +4. **Standard Maven structure** - Follows typical Java library patterns +5. **Fewer files per module** - 16 files per module vs 24 for Android modules diff --git a/README.md b/README.md index 0414a12f..1b28b43b 100644 --- a/README.md +++ b/README.md @@ -1,184 +1,148 @@ +[![Build Status](https://travis-ci.org/cloudinary/cloudinary_java.svg?branch=master)](https://travis-ci.org/cloudinary/cloudinary_java) + Cloudinary ========== -Cloudinary is a cloud service that offers a solution to a web application's entire image management pipeline. - -Easily upload images to the cloud. Automatically perform smart image resizing, cropping and conversion without installing any complex software. -Integrate Facebook or Twitter profile image extraction in a snap, in any dimension and style to match your website’s graphics requirements. -Images are seamlessly delivered through a fast CDN, and much much more. - -Cloudinary offers comprehensive APIs and administration capabilities and is easy to integrate with any web application, existing or new. - -Cloudinary provides URL and HTTP based APIs that can be easily integrated with any Web development framework. - -For Java, Cloudinary provides a library for simplifying the integration even further. - -**Note:** This Java library is intended mainly for Web applications. For **Android** integration, there is a dedicated library with a similar interface: https://github.com/cloudinary/cloudinary_android - -## Getting started guide -![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **Take a look at our [Getting started guide for Java](http://cloudinary.com/documentation/java_integration#getting_started_guide)**. - -## Setup ###################################################################### - -The cloudinary_java library is available in [Maven Central](http://repo1.maven.org/maven/). To use it, add the following dependency to your pom.xml: - - - com.cloudinary - cloudinary - 1.0.8 - - -Alternatively, download cloudinary_java from [here](https://github.com/cloudinary/cloudinary_java/tarball/master) -and see [pom.xml](https://github.com/cloudinary/cloudinary_java/blob/master/pom.xml) for library dependencies. - -## Try it right away - -Sign up for a [free account](https://cloudinary.com/users/register/free) so you can try out image transformations and seamless image delivery through CDN. - -*Note: Replace `demo` in all the following examples with your Cloudinary's `cloud name`.* - -Accessing an uploaded image with the `sample` public ID through a CDN: - - http://res.cloudinary.com/demo/image/upload/sample.jpg - -![Sample](https://res.cloudinary.com/demo/image/upload/w_0.4/sample.jpg "Sample") - -Generating a 150x100 version of the `sample` image and downloading it through a CDN: - - http://res.cloudinary.com/demo/image/upload/w_150,h_100,c_fill/sample.jpg - -![Sample 150x100](https://res.cloudinary.com/demo/image/upload/w_150,h_100,c_fill/sample.jpg "Sample 150x100") - -Converting to a 150x100 PNG with rounded corners of 20 pixels: - - http://res.cloudinary.com/demo/image/upload/w_150,h_100,c_fill,r_20/sample.png +## About +The Cloudinary Java SDK allows you to quickly and easily integrate your application with Cloudinary. +Effortlessly optimize and transform your cloud's assets. + +### Additional documentation +This Readme provides basic installation and usage information. +For the complete documentation, see the [Java SDK Guide](https://cloudinary.com/documentation/java_integration). + +## Table of Contents +- [Key Features](#key-features) +- [Version Support](#Version-Support) +- [Installation](#installation) +- [Usage](#usage) + - [Setup](#Setup) + - [Transform and Optimize Assets](#Transform-and-Optimize-Assets) + - [File upload](#File-upload) + +## Key Features +- [Transform](https://cloudinary.com/documentation/java_video_manipulation) and [optimize](https://cloudinary.com/documentation/java_image_manipulation#image_optimizations) assets (links to docs). +- [Upload assets to cloud](https://cloudinary.com/documentation/java_image_and_video_upload) + +## Version Support +| SDK Version | Java 6+ | Java 8 | +|----------------|---------|--------| +| 1.1.0 - 1.39.0 | V | | +| 2.0.0+ | | V | -![Sample 150x150 Rounded PNG](https://res.cloudinary.com/demo/image/upload/w_150,h_100,c_fill,r_20/sample.png "Sample 150x150 Rounded PNG") - -For plenty more transformation options, see our [image transformations documentation](http://cloudinary.com/documentation/image_transformations). + -Generating a 120x90 thumbnail based on automatic face detection of the Facebook profile picture of Bill Clinton: - - http://res.cloudinary.com/demo/image/facebook/c_thumb,g_face,h_90,w_120/billclinton.jpg - -![Facebook 90x120](https://res.cloudinary.com/demo/image/facebook/c_thumb,g_face,h_90,w_120/billclinton.jpg "Facebook 90x200") +## Installation +The cloudinary_java library is available in [Maven Central](https://mvnrepository.com/artifact/com.cloudinary/cloudinary-core). To use it, add the following dependency to your pom.xml : -For more details, see our documentation for embedding [Facebook](http://cloudinary.com/documentation/facebook_profile_pictures) and [Twitter](http://cloudinary.com/documentation/twitter_profile_pictures) profile pictures. +```xml + + com.cloudinary + cloudinary-http45 + 2.3.1 + +``` ## Usage +### Setup -### Configuration - -Each request for building a URL of a remote cloud resource must have the `cloud_name` parameter set. -Each request to our secure APIs (e.g., image uploads, eager sprite generation) must have the `api_key` and `api_secret` parameters set. -See [API, URLs and access identifiers](http://cloudinary.com/documentation/api_and_access_identifiers) for more details. +Each request for building a URL of a remote cloud resource must have the `cloud_name` parameter set. +Each request to our secure APIs (e.g., image uploads, eager sprite generation) must have the `api_key` and `api_secret` parameters set. +See [API, URLs and access identifiers](https://cloudinary.com/documentation/solution_overview#account_and_api_setup) for more details. -Setting the `cloud_name`, `api_key` and `api_secret` parameters can be done either directly in each call to a Cloudinary method, +Setting the `cloud_name`, `api_key` and `api_secret` parameters can be done either directly in each call to a Cloudinary method, by when initializing the Cloudinary object, or by using the CLOUDINARY_URL environment variable / system property. -The entry point of the library is the Cloudinary object. - - Cloudinary cloudinary = new Cloudinary(); +The entry point of the library is the Cloudinary object. +```java +Cloudinary cloudinary = new Cloudinary(); +``` Here's an example of setting the configuration parameters programatically: - Map config = new HashMap(); - config.put("cloud_name", "n07t21i7"); - config.put("api_key", "123456789012345"); - config.put("api_secret", "abcdeghijklmnopqrstuvwxyz12"); - Cloudinary cloudinary = new Cloudinary(config); +```java +Map config = new HashMap(); +config.put("cloud_name", "n07t21i7"); +config.put("api_key", "123456789012345"); +config.put("api_secret", "abcdeghijklmnopqrstuvwxyz12"); +Cloudinary cloudinary = new Cloudinary(config); +``` Another example of setting the configuration parameters by providing the CLOUDINARY_URL value to the constructor: Cloudinary cloudinary = new Cloudinary("cloudinary://123456789012345:abcdeghijklmnopqrstuvwxyz12@n07t21i7"); -### Embedding and transforming images - +### Transform and Optimize Assets +- [See full documentation](https://cloudinary.com/documentation/java_image_manipulation) Any image uploaded to Cloudinary can be transformed and embedded using powerful view helper methods: The following example generates the url for accessing an uploaded `sample` image while transforming it to fill a 100x150 rectangle: - cloudinary.url().transformation(new Transformation().width(100).height(150).crop("fill")).generate("sample.jpg") +```java +cloudinary.url().transformation(new Transformation().width(100).height(150).crop("fill")).generate("sample.jpg"); +``` -Another example, emedding a smaller version of an uploaded image while generating a 90x90 face detection based thumbnail: +Another example, emedding a smaller version of an uploaded image while generating a 90x90 face detection based thumbnail: - cloudinary.url().transformation(new Transformation().width(90).height(90).crop("thumb").gravity("face")).generate("woman.jpg") +```java +cloudinary.url().transformation(new Transformation().width(90).height(90).crop("thumb").gravity("face")).generate("woman.jpg"); +``` -You can provide either a Facebook name or a numeric ID of a Facebook profile or a fan page. - -Embedding a Facebook profile to match your graphic design is very simple: +You can provide either a Facebook name or a numeric ID of a Facebook profile or a fan page. - cloudinary.url().type("facebook").transformation(new Transformation().width(130).height(130).crop("fill").gravity("north_west")).generate("billclinton.jpg") - -Same goes for Twitter: - - cloudinary.url().type("twitter_name").generate("billclinton.jpg") - -![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/java_image_manipulation) for more information about displaying and transforming images in Java**. +Embedding a Facebook profile to match your graphic design is very simple: -### Upload +```java +cloudinary.url().type("facebook").transformation(new Transformation().width(130).height(130).crop("fill").gravity("north_west")).generate("billclinton.jpg"); +``` +### File upload Assuming you have your Cloudinary configuration parameters defined (`cloud_name`, `api_key`, `api_secret`), uploading to Cloudinary is very simple. - -The following example uploads a local JPG to the cloud: - - cloudinary.uploader().upload("my_picture.jpg", Cloudinary.emptyMap()) - -The uploaded image is assigned a randomly generated public ID. The image is immediately available for download through a CDN: - - cloudinary.url().generate("abcfrmo8zul1mafopawefg.jpg") - - http://res.cloudinary.com/demo/image/upload/abcfrmo8zul1mafopawefg.jpg -You can also specify your own public ID: - - cloudinary.uploader().upload("http://www.example.com/image.jpg", Cloudinary.asMap("public_id", "sample_remote")) +The following example uploads a local JPG to the cloud: - cloudinary.url().generate("sample_remote.jpg") +```java +cloudinary.uploader().upload("my_picture.jpg", ObjectUtils.emptyMap()); +``` - http://res.cloudinary.com/demo/image/upload/sample_remote.jpg - -![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/java_image_upload) for plenty more options of uploading to the cloud from your Java code**. - -### imageTag - -Returns an html image tag pointing to Cloudinary. - -Usage: - - cloudinary.url().format("png").transformation(new Transformation().width(100).height(100).crop("fill")).imageTag("sample") - - # - -### imageUploadTag - -Returns an html input field for direct image upload, to be used in conjunction with [cloudinary\_js package](https://github.com/cloudinary/cloudinary_js/). It integrates [jQuery-File-Upload widget](https://github.com/blueimp/jQuery-File-Upload) and provides all the necessary parameters for a direct upload. +The uploaded image is assigned a randomly generated public ID. The image is immediately available for download through a CDN: -Usage: +```java +cloudinary.url().generate("abcfrmo8zul1mafopawefg.jpg"); - Map options = Cloudinary.asMap("resource_type", "auto"); - Map htmlOptions = Cloudinary.asMap("alt", "sample"); - String html = cloudinary.uploader().imageUploadTag("image_id", options, htmlOptions); +# http://res.cloudinary.com/demo/image/upload/abcfrmo8zul1mafopawefg.jpg +``` -![](http://res.cloudinary.com/cloudinary/image/upload/see_more_bullet.png) **See [our documentation](http://cloudinary.com/documentation/java_image_upload#direct_uploading_from_the_browser) for plenty more options of uploading directly from the browser**. - -## Additional resources ########################################################## +You can also specify your own public ID: -Additional resources are available at: +```java +cloudinary.uploader().upload("http://www.example.com/image.jpg", ObjectUtils.asMap("public_id", "sample_remote")); -* [Website](http://cloudinary.com) -* [Documentation](http://cloudinary.com/documentation) -* [Image transformations documentation](http://cloudinary.com/documentation/image_transformations) -* [Upload API documentation](http://cloudinary.com/documentation/upload_images) +cloudinary.url().generate("sample_remote.jpg"); -## Support +# http://res.cloudinary.com/demo/image/upload/sample_remote.jpg +``` -You can [open an issue through GitHub](https://github.com/cloudinary/cloudinary_java/issues). +## Contributions +See [contributing guidelines](/CONTRIBUTING.md). -Contact us at [info@cloudinary.com](mailto:info@cloudinary.com) +## Get Help +- [Open a Github issue](https://github.com/CloudinaryLtd/cloudinary_java/issues) (for issues related to the SDK) +- [Open a support ticket](https://cloudinary.com/contact) (for issues related to your account) -Or via Twitter: [@cloudinary](https://twitter.com/#!/cloudinary) +## About Cloudinary +Cloudinary is a powerful media API for websites and mobile apps alike, Cloudinary enables developers to efficiently manage, transform, optimize, and deliver images and videos through multiple CDNs. Ultimately, viewers enjoy responsive and personalized visual-media experiences—irrespective of the viewing device. -## License ####################################################################### +## Additional Resources +- [Cloudinary Transformation and REST API References](https://cloudinary.com/documentation/cloudinary_references): Comprehensive references, including syntax and examples for all SDKs. +- [MediaJams.dev](https://mediajams.dev/): Bite-size use-case tutorials written by and for Cloudinary Developers +- [DevJams](https://www.youtube.com/playlist?list=PL8dVGjLA2oMr09amgERARsZyrOz_sPvqw): Cloudinary developer podcasts on YouTube. +- [Cloudinary Academy](https://training.cloudinary.com/): Free self-paced courses, instructor-led virtual courses, and on-site courses. +- [Code Explorers and Feature Demos](https://cloudinary.com/documentation/code_explorers_demos_index): A one-stop shop for all code explorers, Postman collections, and feature demos found in the docs. +- [Cloudinary Roadmap](https://cloudinary.com/roadmap): Your chance to follow, vote, or suggest what Cloudinary should develop next. +- [Cloudinary Facebook Community](https://www.facebook.com/groups/CloudinaryCommunity): Learn from and offer help to other Cloudinary developers. +- [Cloudinary Account Registration](https://cloudinary.com/users/register/free): Free Cloudinary account registration. +- [Cloudinary Website](https://cloudinary.com) -Released under the MIT license. +## Licence +Released under the MIT license. diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..ae30f0db --- /dev/null +++ b/build.gradle @@ -0,0 +1,48 @@ +import groovy.json.JsonSlurper + +plugins { + id 'maven-publish' + // Removed old nexus plugin - we'll create bundles manually for Central Portal +} + +allprojects { + repositories { + mavenCentral() + } + project.ext.set("publishGroupId", group) +} + +// Removed nexusPublishing block - we'll create bundles manually for Central Portal upload + +tasks.create('createTestSubAccount') { + doFirst { + println("Task createTestSubAccount called with module $moduleName") + + def cloudinaryUrl = "" + + // core does not use test clouds, skip (keep empty file for a more readable generic travis test script) + if (moduleName != "core") { + println "Creating test cloud..." + def baseUrl = new URL('https://sub-account-testing.cloudinary.com/create_sub_account') + def connection = baseUrl.openConnection() + connection.with { + doOutput = true + requestMethod = 'POST' + def json = new JsonSlurper().parseText(content.text) + def cloud = json["payload"]["cloudName"] + def key = json["payload"]["cloudApiKey"] + def secret = json["payload"]["cloudApiSecret"] + cloudinaryUrl = "CLOUDINARY_URL=cloudinary://$key:$secret@$cloud" + } + + } + + def dir = new File("${projectDir.path}${File.separator}tools") + dir.mkdir() + def file = new File(dir, "cloudinary_url.txt") + file.createNewFile() + file.text = cloudinaryUrl + + println("Test sub-account created succesfully!") + } +} diff --git a/cloudinary-core/build.gradle b/cloudinary-core/build.gradle new file mode 100644 index 00000000..67379690 --- /dev/null +++ b/cloudinary-core/build.gradle @@ -0,0 +1,16 @@ +plugins { + id 'java-library' +} + +task ciTest( type: Test ) + +dependencies { + testImplementation "org.hamcrest:java-hamcrest:2.0.0.0" + testImplementation group: 'pl.pragmatists', name: 'JUnitParams', version: '1.0.5' + testImplementation group: 'junit', name: 'junit', version: '4.12' +} + +apply from: "../java_shared.gradle" +apply from: "../publish.gradle" + +// Publishing configuration moved to ../publish.gradle \ No newline at end of file diff --git a/cloudinary-core/pom.xml b/cloudinary-core/pom.xml deleted file mode 100644 index a31d9454..00000000 --- a/cloudinary-core/pom.xml +++ /dev/null @@ -1,13 +0,0 @@ - - 4.0.0 - - - com.cloudinary - cloudinary-parent - 1.0.15-SNAPSHOT - - - cloudinary - jar - - diff --git a/cloudinary-core/src/main/java/com/cloudinary/AccessControlRule.java b/cloudinary-core/src/main/java/com/cloudinary/AccessControlRule.java new file mode 100644 index 00000000..e1870d7c --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/AccessControlRule.java @@ -0,0 +1,66 @@ +package com.cloudinary; + +import com.cloudinary.utils.ObjectUtils; +import org.cloudinary.json.JSONObject; + +import java.util.Date; + +/** + * A class representing a single access control rule for a resource. Used as a parameter for {@link Api#update} and {@link Uploader#upload} + */ +public class AccessControlRule extends JSONObject { + + /** + * Construct a new token access rule + * @return The access rule instance + */ + public static AccessControlRule token(){ + return new AccessControlRule(AccessType.token, null, null); + } + + /** + * Construct a new anonymous access rule + * @param start The start date for the rule + * @return The access rule instance + */ + public static AccessControlRule anonymousFrom(Date start){ + return new AccessControlRule(AccessType.anonymous, start, null); + } + + /** + * Construct a new anonymous access rule + * @param end The end date for the rule + * @return The access rule instance + */ + public static AccessControlRule anonymousUntil(Date end){ + return new AccessControlRule(AccessType.anonymous, null, end); + } + + /** + * Construct a new anonymous access rule + * @param start The start date for the rule + * @param end The end date for the rule + * @return The access rule instance + */ + public static AccessControlRule anonymous(Date start, Date end){ + return new AccessControlRule(AccessType.anonymous, start, end); + } + + private AccessControlRule(AccessType accessType, Date start, Date end) { + put("access_type", accessType.name()); + if (start != null) { + put("start", ObjectUtils.toISO8601(start)); + } + + if (end != null) { + put("end", ObjectUtils.toISO8601(end)); + } + } + + /** + * Access type for an access rule + */ + public enum AccessType { + anonymous, token + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/Api.java b/cloudinary-core/src/main/java/com/cloudinary/Api.java index 42e10ba4..c84be0d6 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Api.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Api.java @@ -1,177 +1,31 @@ package com.cloudinary; -import java.io.InputStream; -import java.lang.reflect.Constructor; -import java.net.URI; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.regex.Pattern; -import java.util.regex.Matcher; -import java.util.Date; -import java.text.DateFormat; -import java.text.SimpleDateFormat; - -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.Header; -import org.apache.http.client.methods.HttpDelete; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.client.methods.HttpPut; -import org.apache.http.client.methods.HttpUriRequest; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.conn.ClientConnectionManager; -import org.apache.http.impl.client.DefaultHttpClient; -import org.json.simple.JSONValue; -import org.json.simple.parser.ParseException; - -@SuppressWarnings({ "rawtypes", "unchecked" }) +import java.util.*; + +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.AuthorizationRequired; +import com.cloudinary.api.exceptions.*; +import com.cloudinary.metadata.MetadataField; +import com.cloudinary.metadata.MetadataDataSource; +import com.cloudinary.metadata.MetadataRule; +import com.cloudinary.strategies.AbstractApiStrategy; +import com.cloudinary.utils.Base64Coder; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.cloudinary.json.JSONArray; + +@SuppressWarnings({"rawtypes", "unchecked"}) public class Api { - enum HttpMethod { GET, POST, PUT, DELETE } - - public static class RateLimit { - private long limit = 0L; - private long remaining = 0L; - private Date reset = null; - public RateLimit(){ - super(); - } - - public long getLimit() { - return limit; - } - public void setLimit(long limit) { - this.limit = limit; - } - public long getRemaining() { - return remaining; - } - public void setRemaining(long remaining) { - this.remaining = remaining; - } - public Date getReset() { - return reset; - } - public void setReset(Date reset) { - this.reset = reset; - } - } - - public static interface ApiResponse extends Map { - HttpResponse getRawHttpResponse(); - Map rateLimits() throws java.text.ParseException; - RateLimit apiRateLimit() throws java.text.ParseException; - } - - public static class Response extends HashMap implements ApiResponse { - private static final long serialVersionUID = -5458609797599845837L; - private HttpResponse response = null; - public Response(HttpResponse response, Map result) { - super(result); - this.response = response; - } - - public HttpResponse getRawHttpResponse(){ - return this.response; - } - - private static final Pattern RATE_LIMIT_REGEX = Pattern.compile("X-Feature(\\w*)RateLimit(-Limit|-Reset|-Remaining)"); - private static final String RFC1123_PATTERN = "EEE, dd MMM yyyyy HH:mm:ss z"; - private static final DateFormat RFC1123 = new SimpleDateFormat(RFC1123_PATTERN); - public Map rateLimits() throws java.text.ParseException { - Header[] headers = this.response.getAllHeaders(); - Map limits = new HashMap(); - for (Header header : headers){ - Matcher m = RATE_LIMIT_REGEX.matcher(header.getName()); - if (m.matches()){ - String limitName = "Api"; - RateLimit limit = null; - if (!m.group(1).isEmpty()) { - limitName = m.group(1); - } - limit = limits.get(limitName); - if (limit == null) { - limit = new RateLimit(); - } - if (m.group(2).equalsIgnoreCase("-limit")) { - limit.setLimit(Long.parseLong(header.getValue())); - } else if (m.group(2).equalsIgnoreCase("-remaining")) { - limit.setRemaining(Long.parseLong(header.getValue())); - } else if (m.group(2).equalsIgnoreCase("-reset")) { - limit.setReset(RFC1123.parse(header.getValue())); - } - limits.put(limitName, limit); - } - } - return limits; - } - - public RateLimit apiRateLimit() throws java.text.ParseException { - return rateLimits().get("Api"); - } - } - - public static class ApiException extends Exception { - private static final long serialVersionUID = 4416861825144420038L; - public ApiException(String message) { - super(message); - } - } - - public static class BadRequest extends ApiException { - private static final long serialVersionUID = 1410136354253339531L; - public BadRequest(String message) { - super(message); - } - } - - public static class AuthorizationRequired extends ApiException { - private static final long serialVersionUID = 7160740370855761014L; - public AuthorizationRequired(String message) { - super(message); - } - } - - public static class NotAllowed extends ApiException { - private static final long serialVersionUID = 4371365822491647653L; - public NotAllowed(String message) { - super(message); - } - } - - public static class NotFound extends ApiException { - private static final long serialVersionUID = -2072640462778940357L; - public NotFound(String message) { - super(message); - } - } - public static class AlreadyExists extends ApiException { - private static final long serialVersionUID = 999568182896607322L; - public AlreadyExists(String message) { - super(message); - } - } - public static class RateLimited extends ApiException { - private static final long serialVersionUID = -8298038106172355219L; - public RateLimited(String message) { - super(message); - } + public AbstractApiStrategy getStrategy() { + return strategy; } - public static class GeneralError extends ApiException { - private static final long serialVersionUID = 4553362706625067182L; - public GeneralError(String message) { - super(message); - } - } + public enum HttpMethod {GET, POST, PUT, DELETE;} public final static Map> CLOUDINARY_API_ERROR_CLASSES = new HashMap>(); + static { CLOUDINARY_API_ERROR_CLASSES.put(400, BadRequest.class); CLOUDINARY_API_ERROR_CLASSES.put(401, AuthorizationRequired.class); @@ -182,264 +36,902 @@ public GeneralError(String message) { CLOUDINARY_API_ERROR_CLASSES.put(500, GeneralError.class); } - private final Cloudinary cloudinary; + public final Cloudinary cloudinary; + + private AbstractApiStrategy strategy; + + protected ApiResponse callApi(HttpMethod method, Iterable uri, Map params, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + + String apiKey = ObjectUtils.asString(options.get("api_key"), this.cloudinary.config.apiKey); + String apiSecret = ObjectUtils.asString(options.get("api_secret"), this.cloudinary.config.apiSecret); + String oauthToken = ObjectUtils.asString(options.get("oauth_token"), this.cloudinary.config.oauthToken); - public Api(Cloudinary cloudinary) { + validateAuthorization(apiKey, apiSecret, oauthToken); + + + String authorizationHeader = getAuthorizationHeaderValue(apiKey, apiSecret, oauthToken); + String apiUrl = createApiUrl(uri, options); + return this.strategy.callApi(method, apiUrl, params, options, authorizationHeader); + } + + public Api(Cloudinary cloudinary, AbstractApiStrategy strategy) { this.cloudinary = cloudinary; + this.strategy = strategy; + this.strategy.init(this); } public ApiResponse ping(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("ping"), Cloudinary.emptyMap(), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("ping"), ObjectUtils.emptyMap(), options); } public ApiResponse usage(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("usage"), Cloudinary.emptyMap(), options); + if (options == null) options = ObjectUtils.emptyMap(); + + final List uri = new ArrayList(); + uri.add("usage"); + + Object date = options.get("date"); + + if (date != null) { + if (date instanceof Date) { + date = ObjectUtils.toUsageApiDateFormat((Date) date); + } + + uri.add(date.toString()); + } + + return callApi(HttpMethod.GET, uri, ObjectUtils.emptyMap(), options); + } + + public ApiResponse configuration(Map options) throws Exception { + if(options == null) options = ObjectUtils.emptyMap(); + + final List uri = new ArrayList(); + uri.add("config"); + + Map params = ObjectUtils.only(options, "settings"); + + return callApi(HttpMethod.GET, uri, params, options); } public ApiResponse resourceTypes(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("resources"), Cloudinary.emptyMap(), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("resources"), ObjectUtils.emptyMap(), options); } public ApiResponse resources(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type")); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type")); List uri = new ArrayList(); uri.add("resources"); uri.add(resourceType); if (type != null) uri.add(type); - return callApi(HttpMethod.GET, uri, Cloudinary.only(options, "next_cursor", "direction", "max_results", "prefix", "tags", "context", "moderations", "start_at"), options); + if(options.get("fields") != null) { + options.put("fields", StringUtils.join(ObjectUtils.asArray(options.get("fields")), ",")); + } + ApiResponse response = callApi(HttpMethod.GET, uri, ObjectUtils.only(options, "next_cursor", "direction", "max_results", "prefix", "tags", "context", "moderations", "start_at", "metadata", "fields"), options); + return response; + } + + public ApiResponse visualSearch(Map options) throws Exception { + List uri = new ArrayList(); + uri.add("resources/visual_search"); + uri.add("image"); + if (options.get("text") == null && options.get("image_asset_id") == null && options.get("image_url") == null) { + throw new IllegalArgumentException("Must supply image file, image url, image asset id or text"); + } + ApiResponse response = callApi(HttpMethod.GET, uri, options, options); + return response; } - + public ApiResponse resourcesByTag(String tag, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, "tags", tag), Cloudinary.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations"), options); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + if(options.get("fields") != null) { + options.put("fields", StringUtils.join(ObjectUtils.asArray(options.get("fields")), ",")); + } + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, "tags", tag), ObjectUtils.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations", "metadata", "fields"), options); + return response; + } + + public ApiResponse resourcesByContext(String key, Map options) throws Exception { + return resourcesByContext(key, null, options); + } + + public ApiResponse resourcesByContext(String key, String value, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + if(options.get("fields") != null) { + options.put("fields", StringUtils.join(ObjectUtils.asArray(options.get("fields")), ",")); + } + Map params = ObjectUtils.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations", "metadata", "fields"); + params.put("key", key); + if (StringUtils.isNotBlank(value)) { + params.put("value", value); + } + return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, "context"), params, options); + } + + public ApiResponse resourceByAssetID(String assetId, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + if(options.get("fields") != null) { + options.put("fields", StringUtils.join(ObjectUtils.asArray(options.get("fields")), ",")); + } + Map params = buildResourceDetailParams(options); + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources", assetId), params, options); + return response; + } + public ApiResponse resourcesByAssetIDs(Iterable assetIds, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + Map params = ObjectUtils.only(options, "public_ids", "tags", "context", "moderations"); + params.put("asset_ids", assetIds); + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources", "by_asset_ids"), params, options); + return response; + } + + public ApiResponse resourcesByAssetFolder(String assetFolder, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + if(options.get("fields") != null) { + options.put("fields", StringUtils.join(ObjectUtils.asArray(options.get("fields")), ",")); + } + Map params = ObjectUtils.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations", "fields"); + params.put("asset_folder", assetFolder); + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources/by_asset_folder"), params, options); + return response; } - + public ApiResponse resourcesByIds(Iterable publicIds, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type"), "upload"); - Map params = Cloudinary.only(options, "tags", "context", "moderations"); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map params = ObjectUtils.only(options, "tags", "context", "moderations"); params.put("public_ids", publicIds); - return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, type), params, options); + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, type), params, options); + return response; } - + public ApiResponse resourcesByModeration(String kind, String status, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, "moderations", kind, status), Cloudinary.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations"), options); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + if(options.get("fields") != null) { + options.put("fields", StringUtils.join(ObjectUtils.asArray(options.get("fields")), ",")); + } + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, "moderations", kind, status), ObjectUtils.only(options, "next_cursor", "direction", "max_results", "tags", "context", "moderations", "metadata", "fields"), options); + return response; } public ApiResponse resource(String public_id, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type"), "upload"); - return callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, type, public_id), - Cloudinary.only(options, "exif", "colors", "faces", "coordinates", - "image_metadata", "pages", "phash", "max_results"), options); - } - + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map params = buildResourceDetailParams(options); + + ApiResponse response = callApi(HttpMethod.GET, Arrays.asList("resources", resourceType, type, public_id), params, options); + + return response; + } + + private Map buildResourceDetailParams(Map options) { + return ObjectUtils.only(options, "exif", "colors", "faces", "coordinates", + "image_metadata", "pages", "phash", "max_results", "quality_analysis", "cinemagraph_analysis", + "accessibility_analysis", "versions", "media_metadata", "derived_next_cursor"); + } + public ApiResponse update(String public_id, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type"), "upload"); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); Map params = new HashMap(); Util.processWriteParameters(options, params); params.put("moderation_status", options.get("moderation_status")); - return callApi(HttpMethod.POST, Arrays.asList("resources", resourceType, type, public_id), - params, options); + params.put("notification_url", options.get("notification_url")); + ApiResponse response = callApi(HttpMethod.POST, Arrays.asList("resources", resourceType, type, public_id), + params, options); + return response; } public ApiResponse deleteResources(Iterable publicIds, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type"), "upload"); - Map params = Cloudinary.only(options, "keep_original", "next_cursor"); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map params = ObjectUtils.only(options, "keep_original", "invalidate", "next_cursor", "transformations"); + params.put("public_ids", publicIds); + return callApi(HttpMethod.DELETE, Arrays.asList("resources", resourceType, type), params, options); + } + + public ApiResponse deleteResourcesByAssetIds(Iterable assetIds, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + Map params = ObjectUtils.only(options, "keep_original", "invalidate", "next_cursor", "transformations"); + params.put("asset_ids", assetIds); + return callApi(HttpMethod.DELETE, Arrays.asList("resources"), params, options); + } + + public ApiResponse deleteDerivedByTransformation(Iterable publicIds, List transformations, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map params = ObjectUtils.only(options, "invalidate", "next_cursor"); + params.put("keep_original", true); params.put("public_ids", publicIds); + params.put("transformations", Util.buildEager(transformations)); return callApi(HttpMethod.DELETE, Arrays.asList("resources", resourceType, type), params, options); } public ApiResponse deleteResourcesByPrefix(String prefix, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type"), "upload"); - Map params = Cloudinary.only(options, "keep_original", "next_cursor"); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map params = ObjectUtils.only(options, "keep_original", "invalidate", "next_cursor"); params.put("prefix", prefix); return callApi(HttpMethod.DELETE, Arrays.asList("resources", resourceType, type), params, options); } public ApiResponse deleteResourcesByTag(String tag, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - return callApi(HttpMethod.DELETE, Arrays.asList("resources", resourceType, "tags", tag), Cloudinary.only(options, "keep_original", "next_cursor"), options); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + return callApi(HttpMethod.DELETE, Arrays.asList("resources", resourceType, "tags", tag), ObjectUtils.only(options, "keep_original", "invalidate", "next_cursor"), options); } - + public ApiResponse deleteAllResources(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - String type = Cloudinary.asString(options.get("type"), "upload"); - Map filtered = Cloudinary.only(options, "keep_original", "next_cursor"); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map filtered = ObjectUtils.only(options, "keep_original", "invalidate", "next_cursor"); filtered.put("all", true); return callApi(HttpMethod.DELETE, Arrays.asList("resources", resourceType, type), filtered, options); } public ApiResponse deleteDerivedResources(Iterable derivedResourceIds, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.DELETE, Arrays.asList("derived_resources"), Cloudinary.asMap("derived_resource_ids", derivedResourceIds), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.DELETE, Arrays.asList("derived_resources"), ObjectUtils.asMap("derived_resource_ids", derivedResourceIds), options); } public ApiResponse tags(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String resourceType = Cloudinary.asString(options.get("resource_type"), "image"); - return callApi(HttpMethod.GET, Arrays.asList("tags", resourceType), Cloudinary.only(options, "next_cursor", "max_results", "prefix"), options); + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + return callApi(HttpMethod.GET, Arrays.asList("tags", resourceType), ObjectUtils.only(options, "next_cursor", "max_results", "prefix"), options); } public ApiResponse transformations(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("transformations"), Cloudinary.only(options, "next_cursor", "max_results"), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("transformations"), ObjectUtils.only(options, "next_cursor", "max_results", "named"), options); } public ApiResponse transformation(String transformation, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("transformations", transformation), Cloudinary.only(options, "max_results"), options); + if (options == null) options = ObjectUtils.emptyMap(); + Map map = ObjectUtils.only(options, "next_cursor", "max_results"); + map.put("transformation", transformation); + return callApi(HttpMethod.GET, Arrays.asList("transformations"), map, options); } public ApiResponse deleteTransformation(String transformation, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.DELETE, Arrays.asList("transformations", transformation), Cloudinary.emptyMap(), options); + if (options == null) options = ObjectUtils.emptyMap(); + Map updates = ObjectUtils.asMap("transformation", transformation); + return callApi(HttpMethod.DELETE, Arrays.asList("transformations"), updates, options); } // updates - currently only supported update are: // "allowed_for_strict": boolean flag // "unsafe_update": transformation string public ApiResponse updateTransformation(String transformation, Map updates, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.PUT, Arrays.asList("transformations", transformation), updates, options); + if (options == null) options = ObjectUtils.emptyMap(); + updates.put("transformation", transformation); + return callApi(HttpMethod.PUT, Arrays.asList("transformations"), updates, options); } public ApiResponse createTransformation(String name, String definition, Map options) throws Exception { - return callApi(HttpMethod.POST, Arrays.asList("transformations", name), Cloudinary.asMap("transformation", definition), options); + return callApi(HttpMethod.POST, + Arrays.asList("transformations"), + ObjectUtils.asMap("transformation", definition, "name", name), options); } - + public ApiResponse uploadPresets(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("upload_presets"), Cloudinary.only(options, "next_cursor", "max_results"), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("upload_presets"), ObjectUtils.only(options, "next_cursor", "max_results"), options); } public ApiResponse uploadPreset(String name, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("upload_presets", name), Cloudinary.only(options, "max_results"), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("upload_presets", name), ObjectUtils.only(options, "max_results"), options); } public ApiResponse deleteUploadPreset(String name, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - return callApi(HttpMethod.DELETE, Arrays.asList("upload_presets", name), Cloudinary.emptyMap(), options); + if (options == null) options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.DELETE, Arrays.asList("upload_presets", name), ObjectUtils.emptyMap(), options); } public ApiResponse updateUploadPreset(String name, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); + if (options == null) options = ObjectUtils.emptyMap(); Map params = Util.buildUploadParams(options); Util.clearEmpty(params); - params.putAll(Cloudinary.only(options, "unsigned", "disallow_public_id")); + params.putAll(ObjectUtils.only(options, "unsigned", "disallow_public_id")); return callApi(HttpMethod.PUT, Arrays.asList("upload_presets", name), params, options); } public ApiResponse createUploadPreset(Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); + if (options == null) options = ObjectUtils.emptyMap(); Map params = Util.buildUploadParams(options); Util.clearEmpty(params); - params.putAll(Cloudinary.only(options, "name", "unsigned", "disallow_public_id")); - return callApi(HttpMethod.POST, Arrays.asList("upload_presets"), params, options); - } - - public ApiResponse rootFolders(Map options) throws Exception { - if (options == null) - options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("folders"), Cloudinary.emptyMap(), options); - } - - public ApiResponse subFolders(String ofFolderPath, Map options) throws Exception { - if (options == null) - options = Cloudinary.emptyMap(); - return callApi(HttpMethod.GET, Arrays.asList("folders", ofFolderPath), Cloudinary.emptyMap(), options); - } - - public Api withConnectionManager(ClientConnectionManager connectionManager) { - this.connectionManager = connectionManager; - return this; - } + params.putAll(ObjectUtils.only(options, "name", "unsigned", "disallow_public_id")); + return callApi(HttpMethod.POST, Arrays.asList("upload_presets"), params, options); + } - protected ApiResponse callApi(HttpMethod method, Iterable uri, Map params, Map options) throws Exception { - if (options == null) options = Cloudinary.emptyMap(); - String prefix = Cloudinary.asString(options.get("upload_prefix"), - this.cloudinary.getStringConfig("upload_prefix", "https://api.cloudinary.com")); - String cloudName = Cloudinary.asString(options.get("cloud_name"), this.cloudinary.getStringConfig("cloud_name")); - if (cloudName == null) - throw new IllegalArgumentException("Must supply cloud_name"); - String apiKey = Cloudinary.asString(options.get("api_key"), this.cloudinary.getStringConfig("api_key")); - if (apiKey == null) - throw new IllegalArgumentException("Must supply api_key"); - String apiSecret = Cloudinary.asString(options.get("api_secret"), this.cloudinary.getStringConfig("api_secret")); - if (apiSecret == null) - throw new IllegalArgumentException("Must supply api_secret"); - - String apiUrl = StringUtils.join(Arrays.asList(prefix, "v1_1", cloudName), "/"); - for (String component : uri) { - apiUrl = apiUrl + "/" + component; + public ApiResponse rootFolders(Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("folders"), + extractParams(options, Arrays.asList("max_results", "next_cursor")), + options); + } + + public ApiResponse subFolders(String ofFolderPath, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("folders", ofFolderPath), + extractParams(options, Arrays.asList("max_results", "next_cursor")), + options); + } + + //Creates an empty folder + public ApiResponse createFolder(String folderName, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.POST, Arrays.asList("folders", folderName), ObjectUtils.emptyMap(), options); + } + + public ApiResponse restore(Iterable publicIds, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + Map params = new HashMap(); + params.put("public_ids", publicIds); + params.put("versions", options.get("versions")); + + ApiResponse response = callApi(HttpMethod.POST, Arrays.asList("resources", resourceType, type, "restore"), params, options); + return response; + } + + public ApiResponse restoreByAssetIds(Iterable assetIds, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + params.put("asset_ids", assetIds); + return callApi(HttpMethod.POST, Arrays.asList("resources", "restore"), params, options); + } + + public ApiResponse uploadMappings(Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("upload_mappings"), + ObjectUtils.only(options, "next_cursor", "max_results"), options); + } + + public ApiResponse uploadMapping(String name, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.GET, Arrays.asList("upload_mappings"), ObjectUtils.asMap("folder", name), options); + } + + public ApiResponse deleteUploadMapping(String name, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + return callApi(HttpMethod.DELETE, Arrays.asList("upload_mappings"), ObjectUtils.asMap("folder", name), options); + } + + public ApiResponse updateUploadMapping(String name, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + params.put("folder", name); + params.putAll(ObjectUtils.only(options, "template")); + return callApi(HttpMethod.PUT, Arrays.asList("upload_mappings"), params, options); + } + + public ApiResponse createUploadMapping(String name, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + params.put("folder", name); + params.putAll(ObjectUtils.only(options, "template")); + return callApi(HttpMethod.POST, Arrays.asList("upload_mappings"), params, options); + } + + public ApiResponse publishByPrefix(String prefix, Map options) throws Exception { + return publishResource("prefix", prefix, options); + } + + public ApiResponse publishByTag(String tag, Map options) throws Exception { + return publishResource("tag", tag, options); + } + + public ApiResponse publishByIds(Iterable publicIds, Map options) throws Exception { + return publishResource("public_ids", publicIds, options); + } + + private ApiResponse publishResource(String byKey, Object value, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + List uri = new ArrayList(); + uri.add("resources"); + uri.add(resourceType); + uri.add("publish_resources"); + Map params = new HashMap(); + params.put(byKey, value); + params.putAll(ObjectUtils.only(options, "invalidate", "overwrite", "type")); + return callApi(HttpMethod.POST, uri, params, options); + } + + /** + * Create a new streaming profile + * + * @param name the of the profile + * @param displayName the display name of the profile + * @param representations a collection of Maps with a transformation key + * @param options additional options + * @return the new streaming profile + * @throws Exception an exception + */ + public ApiResponse createStreamingProfile(String name, String displayName, List representations, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + List serializedRepresentations = new ArrayList(representations.size()); + for (Map t : representations) { + final Object transformation = t.get("transformation"); + serializedRepresentations.add(ObjectUtils.asMap("transformation", transformation.toString())); + } + List uri = Collections.singletonList("streaming_profiles"); + final Map params = ObjectUtils.asMap( + "name", name, + "representations", new JSONArray(serializedRepresentations.toArray()) + ); + if (displayName != null) { + params.put("display_name", displayName); } - URIBuilder apiUrlBuilder = new URIBuilder(apiUrl); - for (Map.Entry param : params.entrySet()) { - if (param.getValue() instanceof Iterable) { - for (String single : (Iterable) param.getValue()) { - apiUrlBuilder.addParameter(param.getKey() + "[]", single); - } - } else { - apiUrlBuilder.addParameter(param.getKey(), Cloudinary.asString(param.getValue())); - } + return callApi(HttpMethod.POST, uri, params, options); + } + + /** + * @see Api#createStreamingProfile(String, String, List, Map) + */ + public ApiResponse createStreamingProfile(String name, String displayName, List representations) throws Exception { + return createStreamingProfile(name, displayName, representations, null); + } + + /** + * Get a streaming profile information + * + * @param name the name of the profile to fetch + * @param options additional options + * @return a streaming profile + * @throws Exception an exception + */ + public ApiResponse getStreamingProfile(String name, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + List uri = Arrays.asList("streaming_profiles", name); + + return callApi(HttpMethod.GET, uri, ObjectUtils.emptyMap(), options); + + } + + /** + * @see Api#getStreamingProfile(String, Map) + */ + public ApiResponse getStreamingProfile(String name) throws Exception { + return getStreamingProfile(name, null); + } + + /** + * List Streaming profiles + * + * @param options additional options + * @return a list of all streaming profiles defined for the current cloud + * @throws Exception an exception + */ + public ApiResponse listStreamingProfiles(Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + List uri = Collections.singletonList("streaming_profiles"); + return callApi(HttpMethod.GET, uri, ObjectUtils.emptyMap(), options); + + } + + /** + * @see Api#listStreamingProfiles(Map) + */ + public ApiResponse listStreamingProfiles() throws Exception { + return listStreamingProfiles(null); + } + + /** + * Delete a streaming profile information. Predefined profiles are restored to the default setting. + * + * @param name the name of the profile to delete + * @param options additional options + * @return a streaming profile + * @throws Exception an exception + */ + public ApiResponse deleteStreamingProfile(String name, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + List uri = Arrays.asList("streaming_profiles", name); + + return callApi(HttpMethod.DELETE, uri, ObjectUtils.emptyMap(), options); + + } + + /** + * @see Api#deleteStreamingProfile(String, Map) + */ + public ApiResponse deleteStreamingProfile(String name) throws Exception { + return deleteStreamingProfile(name, null); + } + + /** + * Create a new streaming profile + * + * @param name the of the profile + * @param displayName the display name of the profile + * @param representations a collection of Maps with a transformation key + * @param options additional options + * @return the new streaming profile + * @throws Exception an exception + */ + public ApiResponse updateStreamingProfile(String name, String displayName, List representations, Map options) throws Exception { + if (options == null) + options = ObjectUtils.emptyMap(); + List serializedRepresentations; + final Map params = new HashMap(); + List uri = Arrays.asList("streaming_profiles", name); + + if (representations != null) { + serializedRepresentations = new ArrayList(representations.size()); + for (Map t : representations) { + final Object transformation = t.get("transformation"); + serializedRepresentations.add(ObjectUtils.asMap("transformation", transformation.toString())); + } + params.put("representations", new JSONArray(serializedRepresentations.toArray())); } - DefaultHttpClient client = new DefaultHttpClient(connectionManager); - URI apiUri = apiUrlBuilder.build(); - HttpUriRequest request = null; - switch (method) { - case GET: request = new HttpGet(apiUri); break; - case PUT: request = new HttpPut(apiUri); break; - case POST: request = new HttpPost(apiUri); break; - case DELETE: request = new HttpDelete(apiUri); break; + if (displayName != null) { + params.put("display_name", displayName); } - request.setHeader("Authorization", "Basic " + Base64.encodeBase64String((apiKey + ":" + apiSecret).getBytes())); - request.setHeader("User-Agent", Cloudinary.USER_AGENT); - - HttpResponse response = client.execute(request); - - int code = response.getStatusLine().getStatusCode(); - InputStream responseStream = response.getEntity().getContent(); - String responseData = Uploader.readFully(responseStream); - - Class exceptionClass = CLOUDINARY_API_ERROR_CLASSES.get(code); - if (code != 200 && exceptionClass == null) { - throw new GeneralError("Server returned unexpected status code - " + code + " - " + responseData); + return callApi(HttpMethod.PUT, uri, params, options); + } + + /** + * @see Api#updateStreamingProfile(String, String, List, Map) + */ + public ApiResponse updateStreamingProfile(String name, String displayName, List representations) throws Exception { + return createStreamingProfile(name, displayName, representations); + } + + /** + * Update access mode of one or more resources by prefix + * + * @param accessMode The new access mode, "public" or "authenticated" + * @param prefix The prefix by which to filter applicable resources + * @param options additional options + *
    + *
  • resource_type - (default "image") - the type of resources to modify
  • + *
  • max_results - optional - the maximum resources to process in a single invocation
  • + *
  • next_cursor - optional - provided by a previous call to the method
  • + *
+ * @return a map of the returned values + *
    + *
  • updated - an array of resources
  • + *
  • next_cursor - optional - provided if more resources can be processed
  • + *
+ * @throws ApiException an API exception + */ + public ApiResponse updateResourcesAccessModeByPrefix(String accessMode, String prefix, Map options) throws Exception { + return updateResourcesAccessMode(accessMode, "prefix", prefix, options); + } + + /** + * Update access mode of one or more resources by tag + * + * @param accessMode The new access mode, "public" or "authenticated" + * @param tag The tag by which to filter applicable resources + * @param options additional options + *
    + *
  • resource_type - (default "image") - the type of resources to modify
  • + *
  • max_results - optional - the maximum resources to process in a single invocation
  • + *
  • next_cursor - optional - provided by a previous call to the method
  • + *
+ * @return a map of the returned values + *
    + *
  • updated - an array of resources
  • + *
  • next_cursor - optional - provided if more resources can be processed
  • + *
+ * @throws ApiException an API exception + */ + public ApiResponse updateResourcesAccessModeByTag(String accessMode, String tag, Map options) throws Exception { + return updateResourcesAccessMode(accessMode, "tag", tag, options); + } + + /** + * Delete a folder (must be empty). + * + * @param folder The full path of the folder to delete + * @param options additional options. + * @return The operation result. + * @throws Exception When the folder isn't empty or doesn't exist. + */ + public ApiResponse deleteFolder(String folder, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + List uri = Arrays.asList("folders", folder); + Map params = ObjectUtils.only(options, "skip_backup"); + return callApi(HttpMethod.DELETE, uri, params, options); + } + + /** + * Update access mode of one or more resources by publicIds + * + * @param accessMode The new access mode, "public" or "authenticated" + * @param publicIds A list of public ids of resources to be updated + * @param options additional options + *
    + *
  • resource_type - (default "image") - the type of resources to modify
  • + *
  • max_results - optional - the maximum resources to process in a single invocation
  • + *
  • next_cursor - optional - provided by a previous call to the method
  • + *
+ * @return a map of the returned values + *
    + *
  • updated - an array of resources
  • + *
  • next_cursor - optional - provided if more resources can be processed
  • + *
+ * @throws ApiException an API exception + */ + public ApiResponse updateResourcesAccessModeByIds(String accessMode, Iterable publicIds, Map options) throws Exception { + return updateResourcesAccessMode(accessMode, "public_ids", publicIds, options); + } + + private ApiResponse updateResourcesAccessMode(String accessMode, String byKey, Object value, Map options) throws Exception { + if (options == null) options = ObjectUtils.emptyMap(); + String resourceType = ObjectUtils.asString(options.get("resource_type"), "image"); + String type = ObjectUtils.asString(options.get("type"), "upload"); + List uri = Arrays.asList("resources", resourceType, type, "update_access_mode"); + Map params = ObjectUtils.only(options, "next_cursor", "max_results"); + params.put("access_mode", accessMode); + params.put(byKey, value); + return callApi(HttpMethod.POST, uri, params, options); + } + + /** + * Add a new metadata field definition + * + * @param field The field to add. + * @return A map representing the newly added field. + * @throws Exception + */ + public ApiResponse addMetadataField(MetadataField field) throws Exception { + return callApi(HttpMethod.POST, Collections.singletonList("metadata_fields"), + ObjectUtils.toMap(field), ObjectUtils.asMap("content_type", "json")); + } + + /** + * List all the metadata field definitions (structure, not values) + * + * @return A map containing the list of field definitions maps. + * @throws Exception + */ + public ApiResponse listMetadataFields() throws Exception { + return callApi(HttpMethod.GET, Collections.singletonList("metadata_fields"), Collections.emptyMap(), Collections.emptyMap()); + } + + /** + * Get a metadata field definition by id + * + * @param fieldExternalId The id of the field to retrieve + * @return The fields definitions. + * @throws Exception + */ + public ApiResponse metadataFieldByFieldId(String fieldExternalId) throws Exception { + return callApi(HttpMethod.GET, Arrays.asList("metadata_fields", fieldExternalId), Collections.emptyMap(), Collections.emptyMap()); + } + + /** + * Update the definitions of a single metadata field. + * + * @param fieldExternalId The id of the field to update + * @param field The field definition + * @return The updated fields definition. + * @throws Exception + */ + public ApiResponse updateMetadataField(String fieldExternalId, MetadataField field) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId); + return callApi(HttpMethod.PUT, uri, ObjectUtils.toMap(field), Collections.singletonMap("content_type", "json")); + } + + /** + * Update the datasource entries for a given field + * + * @param fieldExternalId The id of the field to update + * @param entries A list of datasource entries. Existing entries (according to entry id) will be updated, + * new entries will be added. + * @return The updated field definition. + * @throws Exception + */ + public ApiResponse updateMetadataFieldDatasource(String fieldExternalId, List entries) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId, "datasource"); + return callApi(HttpMethod.PUT, uri, Collections.singletonMap("values", entries), Collections.singletonMap("content_type", "json")); + } + + /** + * Delete data source entries for a given field + * + * @param fieldExternalId The id of the field to update + * @param entriesExternalId The ids of all the entries to delete from the data source + * @return The remaining datasource entries. + * @throws Exception + */ + public ApiResponse deleteDatasourceEntries(String fieldExternalId, List entriesExternalId) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId, "datasource"); + return callApi(HttpMethod.DELETE, uri, Collections.singletonMap("external_ids", entriesExternalId), Collections.emptyMap()); + } + + /** + * Restore deleted data source entries for a given field + * + * @param fieldExternalId The id of the field to operate + * @param entriesExternalId The ids of all the entries to restore from the data source + * @return The datasource entries state after restore + * @throws Exception + */ + public ApiResponse restoreDatasourceEntries(String fieldExternalId, List entriesExternalId) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId, "datasource_restore"); + return callApi(HttpMethod.POST, uri, Collections.singletonMap("external_ids", entriesExternalId), Collections.singletonMap("content_type", "json")); + } + + /** + * Delete a field definition. + * + * @param fieldExternalId The id of the field to delete + * @return A map with a "message" key. "ok" value indicates a successful deletion. + * @throws Exception + */ + public ApiResponse deleteMetadataField(String fieldExternalId) throws Exception { + List uri = Arrays.asList("metadata_fields", fieldExternalId); + return callApi(HttpMethod.DELETE, uri, Collections.emptyMap(), Collections.emptyMap()); + } + + /** + * Reorders metadata fields. + * + * @param orderBy Criteria for the order (one of the fields 'label', 'external_id', 'created_at') + * @param direction Optional (gets either asc or desc) + * @param options Additional options + * @return List of metadata fields in their new order + * @throws Exception + */ + public ApiResponse reorderMetadataFields(String orderBy, String direction, Map options) throws Exception { + if (orderBy == null) { + throw new IllegalArgumentException("Must supply orderBy"); } - Map result; - try { - result = (Map) JSONValue.parseWithException(responseData); - } catch (ParseException e) { - throw new RuntimeException("Invalid JSON response from server " + e.getMessage()); + + List uri = Arrays.asList("metadata_fields", "order"); + Map map = ObjectUtils.asMap("order_by", orderBy); + if (direction != null) { + map.put("direction", direction); + } + + return callApi(HttpMethod.PUT, uri, map, options); + } + + public ApiResponse listMetadataRules(Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + final Map params = new HashMap(); + List uri = Arrays.asList("metadata_rules"); + return callApi(HttpMethod.GET, uri, params, options); + } + + public ApiResponse addMetadataRule(MetadataRule rule, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + options.put("content_type", "json"); + final Map params = rule.asMap(); + List uri = Arrays.asList("metadata_rules"); + return callApi(HttpMethod.POST, uri, params, options); + } + + public ApiResponse updateMetadataRule(String externalId, MetadataRule rule, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + options.put("content_type", "json"); + final Map params = rule.asMap(); + List uri = Arrays.asList("metadata_rules", externalId); + return callApi(HttpMethod.PUT, uri, params, options); + } + + public ApiResponse deleteMetadataRule(String externalId, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + List uri = Arrays.asList("metadata_rules", externalId); + return callApi(HttpMethod.DELETE, uri, ObjectUtils.emptyMap(), options); + } + + public ApiResponse analyze(String inputType, String analysisType, String uri, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + List url = Arrays.asList("analysis", "analyze", inputType); + options.put("api_version", "v2"); + options.put("content_type", "json"); + final Map params = new HashMap(); + params.put("analysis_type", analysisType); + params.put("uri", uri); + return callApi(HttpMethod.POST, url, params, options); + } + + public ApiResponse renameFolder(String path, String toPath, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + List url = Arrays.asList("folders", path); + + final Map params = new HashMap(); + params.put("to_folder", toPath); + + return callApi(HttpMethod.PUT, url, params, options); + + } + + public ApiResponse deleteBackedUpAssets(String assetId, String[] versionIds, Map options) throws Exception { + if (options == null || options.isEmpty()) options = ObjectUtils.asMap(); + if (StringUtils.isEmpty(assetId)) { + throw new IllegalArgumentException("AssetId parameter is required"); } - if (code == 200) { - return new Response(response, result); + if (versionIds == null || versionIds.length == 0) { + throw new IllegalArgumentException("VersionIds parameter is required"); + } + + List url = Arrays.asList("resources", "backup", assetId); + + Map params = new HashMap(); + params.put("version_ids[]", StringUtils.join(versionIds, "&")); + + return callApi(HttpMethod.DELETE, url, params, options); + + } + + private Map extractParams(Map options, List keys) { + Map result = new HashMap(); + for (String key : keys) { + Object option = options.get(key); + + if (option != null) { + result.put(key, option); + } + } + return result; + } + + protected void validateAuthorization(String apiKey, String apiSecret, String oauthToken) { + if (oauthToken == null) { + if (apiKey == null) throw new IllegalArgumentException("Must supply api_key"); + if (apiSecret == null) throw new IllegalArgumentException("Must supply api_secret"); + } + } + + protected String getAuthorizationHeaderValue(String apiKey, String apiSecret, String oauthToken) { + if (oauthToken != null){ + return "Bearer " + oauthToken; } else { - String message = (String) ((Map) result.get("error")).get("message"); - Constructor exceptionConstructor = exceptionClass.getConstructor(String.class); - throw exceptionConstructor.newInstance(message); + return "Basic " + Base64Coder.encodeString(apiKey + ":" + apiSecret); + } + } + + protected String createApiUrl (Iterable uri, Map options){ + String version = ObjectUtils.asString(options.get("api_version"), "v1_1"); + String prefix = ObjectUtils.asString(options.get("upload_prefix"), ObjectUtils.asString(this.cloudinary.config.uploadPrefix, "https://api.cloudinary.com")); + String cloudName = ObjectUtils.asString(options.get("cloud_name"), this.cloudinary.config.cloudName); + if (cloudName == null) throw new IllegalArgumentException("Must supply cloud_name"); + String apiUrl = StringUtils.join(Arrays.asList(prefix, version, cloudName), "/"); + for (String component : uri) { + component = SmartUrlEncoder.encode(component); + apiUrl = apiUrl + "/" + component; + } + return apiUrl; } - - private ClientConnectionManager connectionManager = null; } diff --git a/cloudinary-core/src/main/java/com/cloudinary/ArchiveParams.java b/cloudinary-core/src/main/java/com/cloudinary/ArchiveParams.java new file mode 100644 index 00000000..8c094f36 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/ArchiveParams.java @@ -0,0 +1,251 @@ +package com.cloudinary; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class ArchiveParams { + public static final String FORMAT_ZIP = "zip"; + + public static final String MODE_DOWNLOAD = "download"; + public static final String MODE_CREATE = "create"; + + private String resourceType = "image"; + private String type = null; + private String mode = MODE_CREATE; + private String targetFormat = null; + private String targetPublicId = null; + private boolean flattenFolders = false; + private boolean flattenTransformations = false; + private boolean useOriginalFilename = false; + private boolean async = false; + private boolean keepDerived = false; + private boolean skipTransformationName = false; + private boolean allowMissing = false; + private String notificationUrl = null; + private String[] targetTags = null; + private String[] tags = null; + private String[] publicIds = null; + private String[] fullyQualifiedPublicIds = null; + private String[] prefixes = null; + private Transformation[] transformations = null; + private Long expiresAt = null; + + public String resourceType() { + return resourceType; + } + + public ArchiveParams resourceType(String resourceType) { + if (resourceType == null) + throw new IllegalArgumentException("resource type must be non-null"); + this.resourceType = resourceType; + return this; + } + + public String type() { + return type; + } + + public ArchiveParams type(String type) { + this.type = type; + return this; + } + + public String mode() { + return mode; + } + + public ArchiveParams mode(String mode) { + this.mode = mode; + return this; + } + + public String targetFormat() { + return targetFormat; + } + + public ArchiveParams targetFormat(String targetFormat) { + this.targetFormat = targetFormat; + return this; + } + + public String targetPublicId() { + return targetPublicId; + } + + public ArchiveParams targetPublicId(String targetPublicId) { + this.targetPublicId = targetPublicId; + return this; + } + + public boolean isFlattenFolders() { + return flattenFolders; + } + + public ArchiveParams flattenFolders(boolean flattenFolders) { + this.flattenFolders = flattenFolders; + return this; + } + + public boolean isFlattenTransformations() { + return flattenTransformations; + } + + public ArchiveParams flattenTransformations(boolean flattenTransformations) { + this.flattenTransformations = flattenTransformations; + return this; + } + + public boolean isUseOriginalFilename() { + return useOriginalFilename; + } + + public ArchiveParams useOriginalFilename(boolean useOriginalFilename) { + this.useOriginalFilename = useOriginalFilename; + return this; + } + + public boolean isAsync() { + return async; + } + + public ArchiveParams async(boolean async) { + this.async = async; + return this; + } + + public boolean isSkipTransformationName() { + return skipTransformationName; + } + + public ArchiveParams skipTransformationName(boolean skipTransformationName) { + this.skipTransformationName = skipTransformationName; + return this; + } + + public boolean isAllowMissing(){ + return allowMissing; + } + + public ArchiveParams allowMissing(boolean allowMissing){ + this.allowMissing = allowMissing; + return this; + } + + public boolean isKeepDerived() { + return keepDerived; + } + + public ArchiveParams keepDerived(boolean keepDerived) { + this.keepDerived = keepDerived; + return this; + } + + public String notificationUrl() { + return notificationUrl; + } + + public ArchiveParams notificationUrl(String notificationUrl) { + this.notificationUrl = notificationUrl; + return this; + } + + public String[] targetTags() { + return targetTags; + } + + public ArchiveParams targetTags(String[] targetTags) { + this.targetTags = targetTags; + return this; + } + + public String[] tags() { + return tags; + } + + public ArchiveParams tags(String[] tags) { + this.tags = tags; + return this; + } + + public String[] publicIds() { + return publicIds; + } + + public ArchiveParams publicIds(String[] publicIds) { + this.publicIds = publicIds; + return this; + } + + public String[] fully_qualified_public_ids() { + return fullyQualifiedPublicIds; + } + + public ArchiveParams fullyQualifiedPublicIds(String[] fullyQualifiedPublicIds) { + this.fullyQualifiedPublicIds = fullyQualifiedPublicIds; + return this; + } + + public String[] prefixes() { + return prefixes; + } + + public ArchiveParams prefixes(String[] prefixes) { + this.prefixes = prefixes; + return this; + } + + public Transformation[] transformations() { + return transformations; + } + + public ArchiveParams transformations(Transformation[] transformations) { + this.transformations = transformations; + return this; + } + + public ArchiveParams expiresAt(Long expiresAt) { + this.expiresAt = expiresAt; + return this; + } + + public Long expiresAt(){ + return expiresAt; + } + + public Map toMap() { + Map params = new HashMap(); + params.put("resource_type", resourceType); + params.put("type", type); + params.put("mode", mode); + if (targetPublicId != null) + params.put("target_public_id", targetPublicId); + params.put("flatten_folders", flattenFolders); + params.put("flatten_transformations", flattenTransformations); + params.put("use_original_filename", useOriginalFilename); + params.put("async", async); + params.put("keep_derived", keepDerived); + params.put("skip_transformation_name", skipTransformationName); + params.put("allow_missing", allowMissing); + if (notificationUrl != null) + params.put("notification_url", notificationUrl); + if (targetTags != null) + params.put("target_tags", targetTags); + if (tags != null) + params.put("tags", tags); + if (publicIds != null) + params.put("public_ids", publicIds); + if(fullyQualifiedPublicIds !=null){ + params.put("fully_qualified_public_ids", fullyQualifiedPublicIds); + } + if (prefixes != null) + params.put("prefixes", prefixes); + if (transformations != null) { + params.put("transformations", Arrays.asList(transformations)); + } + if (expiresAt != null){ + params.put("expires_at", expiresAt); + } + return params; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/AuthToken.java b/cloudinary-core/src/main/java/com/cloudinary/AuthToken.java new file mode 100644 index 00000000..a5114dd3 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/AuthToken.java @@ -0,0 +1,293 @@ +package com.cloudinary; + +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.Charset; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.*; +import java.util.regex.Pattern; + +/** + * Authentication Token generator + */ +public class AuthToken { + /** + * A null AuthToken, which can be passed to a method to override global settings. + */ + public static final AuthToken NULL_AUTH_TOKEN = new AuthToken().setNull(); + private static final String AUTH_TOKEN_NAME = "__cld_token__"; + + private String tokenName = AUTH_TOKEN_NAME; + private String key; + private long startTime; + private long expiration; + private String ip; + private List acl = new ArrayList<>(); + private long duration; + private boolean isNullToken = false; + private static final Pattern UNSAFE_URL_CHARS_PATTERN = Pattern.compile("[ \"#%&'/:;<=>?@\\[\\\\\\]^`{|}~]"); + + public AuthToken() { + } + + public AuthToken(String key) { + this.key = key; + } + + /** + * Create a new AuthToken configuration. + * + * @param options The following keys may be used in the options: key, startTime, expiration, ip, acl, duration. + */ + public AuthToken(Map options) { + if (options != null) { + this.tokenName = ObjectUtils.asString(options.get("tokenName"), this.tokenName); + this.key = (String) options.get("key"); + this.startTime = ObjectUtils.asLong(options.get("startTime"), 0L); + this.expiration = ObjectUtils.asLong(options.get("expiration"), 0L); + this.ip = (String) options.get("ip"); + + Object acl = options.get("acl"); + if (acl != null) { + if (acl instanceof String) { + this.acl = Collections.singletonList(acl.toString()); + } else if (Collection.class.isAssignableFrom(acl.getClass())) { + this.acl = new ArrayList((Collection)acl); + } + } + + this.duration = ObjectUtils.asLong(options.get("duration"), 0L); + } + } + + public Map asMap(){ + Map result = new HashMap(); + + result.put("tokenName", this.tokenName); + result.put("key", this.key); + result.put("startTime", this.startTime); + result.put("expiration", this.expiration); + result.put("ip", this.ip); + result.put("acl", this.acl); + result.put("duration", this.duration); + + return result; + } + + /** + * Create a new AuthToken configuration overriding the default token name. + * + * @param tokenName the name of the token. must be supported by the server. + * @return this + */ + public AuthToken tokenName(String tokenName) { + this.tokenName = tokenName; + return this; + } + + /** + * Set the start time of the token. Defaults to now. + * + * @param startTime in seconds since epoch + * @return this + */ + public AuthToken startTime(long startTime) { + this.startTime = startTime; + return this; + } + + /** + * Set the end time (expiration) of the token + * + * @param expiration in seconds since epoch + * @return this + */ + public AuthToken expiration(long expiration) { + this.expiration = expiration; + return this; + } + + /** + * Set the ip of the client + * + * @param ip + * @return this + */ + public AuthToken ip(String ip) { + this.ip = ip; + return this; + } + + /** + * Define an ACL for a cookie token + * + * @param acl + * @return this + */ + public AuthToken acl(String... acl) { + this.acl = Arrays.asList(acl); + return this; + } + + /** + * The duration of the token in seconds. This value is used to calculate the expiration of the token. + * It is ignored if expiration is provided. + * + * @param duration in seconds + * @return this + */ + public AuthToken duration(long duration) { + this.duration = duration; + return this; + } + + /** + * Generate the authentication token + * + * @return a signed token + */ + public String generate() { + return generate(null); + } + + /** + * Generate a URL token for the given URL. + * + * @param url the URL to be authorized + * @return a URL token + */ + public String generate(String url) { + + if (url == null && (acl == null || acl.size() == 0)) { + throw new IllegalArgumentException("Must provide acl or url"); + } + + long expiration = this.expiration; + if (expiration == 0) { + if (duration > 0) { + final long start = startTime > 0 ? startTime : Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L; + expiration = start + duration; + } else { + throw new IllegalArgumentException("Must provide either expiration or duration"); + } + } + ArrayList tokenParts = new ArrayList(); + if (ip != null) { + tokenParts.add("ip=" + ip); + } + if (startTime > 0) { + tokenParts.add("st=" + startTime); + } + tokenParts.add("exp=" + expiration); + if (acl != null && acl.size() > 0) { + tokenParts.add("acl=" + escapeToLower(String.join("!", acl))); + } + ArrayList toSign = new ArrayList(tokenParts); + if (url != null && (acl == null || acl.size() == 0)) { + toSign.add("url=" + escapeToLower(url)); + } + String auth = digest(StringUtils.join(toSign, "~")); + tokenParts.add("hmac=" + auth); + return tokenName + "=" + StringUtils.join(tokenParts, "~"); + + } + + /** + * Escape url using lowercase hex code + * + * @param url a url string + * @return escaped url + */ + private String escapeToLower(String url) { + String encodedUrl = StringUtils.urlEncode(url, UNSAFE_URL_CHARS_PATTERN, Charset.forName("UTF-8")); + return encodedUrl; + } + + /** + * Create a copy of this AuthToken + * + * @return a new AuthToken object + */ + public AuthToken copy() { + final AuthToken authToken = new AuthToken(key); + authToken.tokenName = tokenName; + authToken.startTime = startTime; + authToken.expiration = expiration; + authToken.ip = ip; + authToken.acl = acl; + authToken.duration = duration; + return authToken; + } + + /** + * Merge this token with another, creating a new token. Other's members who are not null or 0 will override this object's members. + * + * @param other the token to merge from + * @return a new token + */ + public AuthToken merge(AuthToken other) { + if (other.equals(NULL_AUTH_TOKEN)) { + // NULL_AUTH_TOKEN can't merge + return other; + } + AuthToken merged = new AuthToken(); + merged.key = other.key != null ? other.key : this.key; + merged.tokenName = other.tokenName != null ? other.tokenName : this.tokenName; + merged.startTime = other.startTime != 0 ? other.startTime : this.startTime; + merged.expiration = other.expiration != 0 ? other.expiration : this.expiration; + merged.ip = other.ip != null ? other.ip : this.ip; + merged.acl = other.acl != null ? other.acl : this.acl; + merged.duration = other.duration != 0 ? other.duration : this.duration; + return merged; + } + + private String digest(String message) { + byte[] binKey = StringUtils.hexStringToByteArray(key); + try { + Mac hmac = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret = new SecretKeySpec(binKey, "HmacSHA256"); + hmac.init(secret); + final byte[] bytes = message.getBytes(); + return StringUtils.encodeHexString(hmac.doFinal(bytes)).toLowerCase(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Cannot create authorization token.", e); + } catch (InvalidKeyException e) { + throw new RuntimeException("Cannot create authorization token.", e); + } + } + + private AuthToken setNull() { + isNullToken = true; + return this; + } + + @Override + public boolean equals(Object o) { + if (o instanceof AuthToken) { + AuthToken other = (AuthToken) o; + return (isNullToken && other.isNullToken) || + (key == null ? other.key == null : key.equals(other.key)) && + tokenName.equals(other.tokenName) && + startTime == other.startTime && + expiration == other.expiration && + duration == other.duration && + (ip == null ? other.ip == null : ip.equals(other.ip)) && + (acl == null ? other.acl == null : acl.equals(other.acl)); + } else { + return false; + } + } + + @Override + public int hashCode() { + if (isNullToken) { + return 0; + } else { + return Arrays.asList(tokenName, startTime, expiration, duration, ip, acl).hashCode(); + } + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/BaseParam.java b/cloudinary-core/src/main/java/com/cloudinary/BaseParam.java new file mode 100644 index 00000000..a8eb0482 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/BaseParam.java @@ -0,0 +1,22 @@ +package com.cloudinary; + +import com.cloudinary.utils.StringUtils; + +import java.util.List; + +public class BaseParam { + private String param; + + protected BaseParam(List components) { + this.param = StringUtils.join(components, ":"); + } + + protected BaseParam(String... components) { + this.param = StringUtils.join(components, ":"); + } + + @Override + public String toString() { + return param; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java b/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java index c92ad878..1e69c4fe 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java @@ -1,295 +1,397 @@ package com.cloudinary; +import com.cloudinary.api.signing.ApiResponseSignatureVerifier; +import com.cloudinary.api.signing.NotificationRequestSignatureVerifier; +import com.cloudinary.strategies.AbstractApiStrategy; +import com.cloudinary.strategies.AbstractUploaderStrategy; +import com.cloudinary.strategies.StrategyLoader; +import com.cloudinary.utils.Analytics; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import java.io.IOException; import java.io.UnsupportedEncodingException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URLDecoder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.net.URLEncoder; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.lang.StringUtils; -import org.apache.http.client.utils.URIBuilder; -import org.apache.http.conn.ClientConnectionManager; - -@SuppressWarnings({ "rawtypes", "unchecked" }) +import java.util.*; + +import static com.cloudinary.Util.buildMultiParams; + +@SuppressWarnings({"rawtypes", "unchecked"}) public class Cloudinary { - public final static String CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"; - public final static String OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"; - public final static String AKAMAI_SHARED_CDN = "res.cloudinary.com"; - public final static String SHARED_CDN = AKAMAI_SHARED_CDN; - - public final static String VERSION = "1.0.14"; - public final static String USER_AGENT = "cld-java-" + VERSION; - - private final Map config = new HashMap(); - private ClientConnectionManager connectionManager = null; - - public Cloudinary(Map config) { - this.config.putAll(config); - } - - public Cloudinary(String cloudinaryUrl) { - initFromUrl(cloudinaryUrl); - } - - public Cloudinary() { - String cloudinaryUrl = System.getProperty("CLOUDINARY_URL", System.getenv("CLOUDINARY_URL")); - if (cloudinaryUrl != null) { - initFromUrl(cloudinaryUrl); - } - - } - - public Url url() { - return new Url(this); - } - - public Uploader uploader() { - return new Uploader(this).withConnectionManager(connectionManager); - } - - public Api api() { - return new Api(this).withConnectionManager(connectionManager); - } - - public String cloudinaryApiUrl(String action, Map options) { - String cloudinary = asString(options.get("upload_prefix"), asString(this.config.get("upload_prefix"), "https://api.cloudinary.com")); - String cloud_name = asString(options.get("cloud_name"), asString(this.config.get("cloud_name"))); - if (cloud_name == null) - throw new IllegalArgumentException("Must supply cloud_name in tag or in configuration"); - String resource_type = asString(options.get("resource_type"), "image"); - return StringUtils.join(new String[] { cloudinary, "v1_1", cloud_name, resource_type, action }, "/"); - } - - private final static SecureRandom RND = new SecureRandom(); - - public String randomPublicId() { - byte[] bytes = new byte[8]; - RND.nextBytes(bytes); - return Hex.encodeHexString(bytes); - } - - public String signedPreloadedImage(Map result) { - return result.get("resource_type") + "/upload/v" + result.get("version") + "/" + result.get("public_id") - + (result.containsKey("format") ? "." + result.get("format") : "") + "#" + result.get("signature"); - } - - public String apiSignRequest(Map paramsToSign, String apiSecret) { - Collection params = new ArrayList(); - for (Map.Entry param : new TreeMap(paramsToSign).entrySet()) { - if (param.getValue() instanceof Collection) { - params.add(param.getKey() + "=" + StringUtils.join((Collection) param.getValue(), ",")); - } else { - String value = param.getValue().toString(); - if (StringUtils.isNotBlank(value)) { - params.add(param.getKey() + "=" + value); - } - } - } - String to_sign = StringUtils.join(params, "&"); - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-1"); - } catch (NoSuchAlgorithmException e) { - throw new RuntimeException("Unexpected exception", e); - } - byte[] digest = md.digest((to_sign + apiSecret).getBytes()); - return Hex.encodeHexString(digest); - } - - public void signRequest(Map params, Map options) { - String apiKey = Cloudinary.asString(options.get("api_key"), this.getStringConfig("api_key")); - if (apiKey == null) - throw new IllegalArgumentException("Must supply api_key"); - String apiSecret = Cloudinary.asString(options.get("api_secret"), this.getStringConfig("api_secret")); - if (apiSecret == null) - throw new IllegalArgumentException("Must supply api_secret"); - Util.clearEmpty(params); - params.put("signature", this.apiSignRequest(params, apiSecret)); - params.put("api_key", apiKey); - } - - public String privateDownload(String publicId, String format, Map options) throws URISyntaxException { - Map params = new HashMap(); - params.put("public_id", publicId); - params.put("format", format); - params.put("attachment", options.get("attachment")); - params.put("type", options.get("type")); - params.put("timestamp", new Long(System.currentTimeMillis() / 1000L).toString()); - signRequest(params, options); - URIBuilder builder = new URIBuilder(cloudinaryApiUrl("download", options)); - for (Map.Entry param : params.entrySet()) { - builder.addParameter(param.getKey(), param.getValue().toString()); - } - return builder.toString(); - } - - public String zipDownload(String tag, Map options) throws URISyntaxException { - Map params = new HashMap(); - params.put("timestamp", new Long(System.currentTimeMillis() / 1000L).toString()); - params.put("tag", tag); - Object transformation = options.get("transformation"); - if (transformation != null) { - if (transformation instanceof Transformation) { - transformation = ((Transformation) transformation).generate(); - } - params.put("transformation", transformation.toString()); - } - params.put("transformation", transformation); - signRequest(params, options); - URIBuilder builder = new URIBuilder(cloudinaryApiUrl("download_tag.zip", options)); - for (Map.Entry param : params.entrySet()) { - builder.addParameter(param.getKey(), param.getValue().toString()); - } - return builder.toString(); - } - - protected void initFromUrl(String cloudinaryUrl) { - URI cloudinaryUri = URI.create(cloudinaryUrl); - setConfig("cloud_name", cloudinaryUri.getHost()); - String[] creds = cloudinaryUri.getUserInfo().split(":"); - setConfig("api_key", creds[0]); - setConfig("api_secret", creds[1]); - setConfig("private_cdn", StringUtils.isNotBlank(cloudinaryUri.getPath())); - setConfig("secure_distribution", cloudinaryUri.getPath()); - if (cloudinaryUri.getQuery() != null) { - for (String param : cloudinaryUri.getQuery().split("&")) { - String[] keyValue = param.split("="); - try { - setConfig(keyValue[0], URLDecoder.decode(keyValue[1], "ASCII")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException("Unexpected exception", e); - } - } - } - } - - public boolean getBooleanConfig(String key, boolean default_value) { - return asBoolean(this.config.get(key), default_value); - } - - public String getStringConfig(String key, String default_value) { - return asString(this.config.get(key), default_value); - } - - public String getStringConfig(String key) { - return asString(this.config.get(key)); - } - - public void setConfig(String key, Object value) { - this.config.put(key, value); - } - - public Cloudinary withConnectionManager(ClientConnectionManager connectionManager) { - this.connectionManager = connectionManager; - return this; - } - - public static String asString(Object value) { - if (value == null) { - return null; - } else { - return value.toString(); - } - } - - public static String asString(Object value, String defaultValue) { - if (value == null) { - return defaultValue; - } else { - return value.toString(); - } - } - - public static List asArray(Object value) { - if (value == null) { - return Collections.EMPTY_LIST; - } else if (value instanceof int[]) { - List array = new ArrayList(); - for (int i : (int[]) value) { - array.add(new Integer(i)); - } - return array; - } else if (value instanceof Object[]) { - return Arrays.asList((Object[]) value); - } else if (value instanceof List) { - return (List) value; - } else { - List array = new ArrayList(); - array.add(value); - return array; - } - } - - public static Boolean asBoolean(Object value, Boolean defaultValue) { - if (value == null) { - return defaultValue; - } else if (value instanceof Boolean) { - return (Boolean) value; - } else { - return "true".equals(value); - } - } - - public static Float asFloat(Object value) { - if (value == null) { - return null; - } else if (value instanceof Float) { - return (Float) value; - } else { - return Float.parseFloat(value.toString()); - } - } - - public static Map asMap(Object... values) { - if (values.length % 2 != 0) - throw new RuntimeException("Usage - (key, value, key, value, ...)"); - Map result = new HashMap(values.length / 2); - for (int i = 0; i < values.length; i += 2) { - result.put(values[i], values[i + 1]); - } - return result; - } - - public static Map emptyMap() { - return Collections.EMPTY_MAP; - } - - public static String encodeMap(Object arg) { - if (arg != null && arg instanceof Map) { - Map mapArg = (Map) arg; - HashSet out = new HashSet(); - for (Map.Entry entry : mapArg.entrySet()) { - out.add(entry.getKey() + "=" + entry.getValue()); - } - return StringUtils.join(out.toArray(), "|"); - } else if (arg == null) { - return null; - } else { - return arg.toString(); - } - } - - public static Map only(Map hash, String... keys) { - Map result = new HashMap(); - for (String key : keys) { - if (hash.containsKey(key)) { - result.put(key, hash.get(key)); + + public static List UPLOAD_STRATEGIES = new ArrayList(Arrays.asList( + "com.cloudinary.android.UploaderStrategy", + "com.cloudinary.http5.UploaderStrategy")); + public static List API_STRATEGIES = new ArrayList(Arrays.asList( + "com.cloudinary.android.ApiStrategy", + "com.cloudinary.http5.ApiStrategy")); + + public final static String CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"; + public final static String OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"; + public final static String AKAMAI_SHARED_CDN = "res.cloudinary.com"; + public final static String SHARED_CDN = AKAMAI_SHARED_CDN; + + public final static String VERSION = "2.3.1"; + static String USER_AGENT_PREFIX = "CloudinaryJava"; + public final static String USER_AGENT_JAVA_VERSION = "(Java " + System.getProperty("java.version") + ")"; + + public final Configuration config; + private AbstractUploaderStrategy uploaderStrategy; + private AbstractApiStrategy apiStrategy; + private String userAgent = USER_AGENT_PREFIX+"/"+ VERSION + " "+USER_AGENT_JAVA_VERSION; + public Analytics analytics = new Analytics(); + public Uploader uploader() { + return new Uploader(this, uploaderStrategy); + } + + public Api api() { + return new Api(this, apiStrategy); + } + + public Search search() { + return new Search(this); + } + + public SearchFolders searchFolders() { + return new SearchFolders(this); + } + + public static void registerUploaderStrategy(String className) { + if (!UPLOAD_STRATEGIES.contains(className)) { + UPLOAD_STRATEGIES.add(0, className); + } + + } + + public static void registerAPIStrategy(String className) { + if (!API_STRATEGIES.contains(className)) { + API_STRATEGIES.add(0, className); + } + } + + private void loadStrategies() { + if (!this.config.loadStrategies) return; + uploaderStrategy = StrategyLoader.find(UPLOAD_STRATEGIES); + + if (uploaderStrategy == null) { + throw new UnknownError("Can't find Cloudinary platform adapter [" + StringUtils.join(UPLOAD_STRATEGIES, ",") + "]"); + } + + apiStrategy = StrategyLoader.find(API_STRATEGIES); + if (apiStrategy == null) { + throw new UnknownError("Can't find Cloudinary platform adapter [" + StringUtils.join(API_STRATEGIES, ",") + "]"); + } + } + + public Cloudinary(Map config) { + this(new Configuration(config)); + } + + public Cloudinary(String cloudinaryUrl) { + this(Configuration.from(cloudinaryUrl)); + } + + public Cloudinary() { + this(System.getProperty("CLOUDINARY_URL", System.getenv("CLOUDINARY_URL")) != null + ? Configuration.from(System.getProperty("CLOUDINARY_URL", System.getenv("CLOUDINARY_URL"))) + : new Configuration()); + } + + public Cloudinary(Configuration config) { + this.config = config; + loadStrategies(); + } + + public Url url() { + return new Url(this); + } + + public String cloudinaryApiUrl(String action, Map options) { + String cloudinary = ObjectUtils.asString(options.get("upload_prefix"), + ObjectUtils.asString(this.config.uploadPrefix, "https://api.cloudinary.com")); + String cloud_name = ObjectUtils.asString(options.get("cloud_name"), ObjectUtils.asString(this.config.cloudName)); + if (cloud_name == null) + throw new IllegalArgumentException("Must supply cloud_name in tag or in configuration"); + String resource_type = ObjectUtils.asString(options.get("resource_type"), "image"); + return StringUtils.join(new String[]{cloudinary, "v1_1", cloud_name, resource_type, action}, "/"); + } + + private final static SecureRandom RND = new SecureRandom(); + + public String randomPublicId() { + byte[] bytes = new byte[8]; + RND.nextBytes(bytes); + return StringUtils.encodeHexString(bytes); + } + + public String signedPreloadedImage(Map result) { + return result.get("resource_type") + "/upload/v" + result.get("version") + "/" + result.get("public_id") + + (result.containsKey("format") ? "." + result.get("format") : "") + "#" + result.get("signature"); + } + + public String apiSignRequest(Map paramsToSign, String apiSecret, int signatureVersion) { + return Util.produceSignature(paramsToSign, apiSecret, config.signatureAlgorithm, signatureVersion); + } + + /** + * @return the userAgent that will be sent with every API call. + */ + public String getUserAgent(){ + return userAgent; + } + + /** + * Set the prefix and version for the user agent that will be sent with every API call + * a userAgent is built from `prefix/version (additional data)` + * @param prefix - the prefix of the userAgent to be set + * @param version - the version of the userAgent to be set + */ + public void setUserAgent(String prefix, String version){ + userAgent = prefix+"/"+ version + " ("+USER_AGENT_PREFIX+ " "+VERSION+") " + USER_AGENT_JAVA_VERSION; + } + + /** + * Set the analytics object that will be sent with every URL generation call. + * @param analytics - the analytics object to set + */ + public void setAnalytics(Analytics analytics) { + this.analytics = analytics; + } + + /** + * Verifies that Cloudinary notification request is genuine by checking its signature. + * + * Cloudinary can asynchronously process your e.g. image uploads requests. This is achieved by calling back API you + * specified during preparing of upload request as soon as it has been processed. See Upload Notifications in + * Cloudinary documentation for more details. In order to make sure it is Cloudinary calling your API back, hashed + * message authentication codes (HMAC's) based on agreed hashing function and configured Cloudinary API secret key + * are used for signing the requests. + * + * The following method serves as a convenient utility to perform the verification procedure. + * + * @param body Cloudinary Notification request body represented as string + * @param timestamp Cloudinary Notification request custom X-Cld-Timestamp HTTP header value + * @param signature Cloudinary Notification request custom X-Cld-Signature HTTP header value, i.e. the HMAC + * @param validFor desired period of request validity since issued, in seconds, for protection against replay attacks + * @return whether request signature is valid or not + */ + public boolean verifyNotificationSignature(String body, String timestamp, String signature, long validFor) { + return new NotificationRequestSignatureVerifier(config.apiSecret, config.signatureAlgorithm).verifySignature(body, timestamp, signature, validFor); + } + + /** + * Verifies that Cloudinary API response is genuine by checking its signature. + * + * Cloudinary can add a signature value in the response to API methods returning public id's and versions. In order + * to make sure it is genuine Cloudinary response, hashed message authentication codes (HMAC's) based on agreed hashing + * function and configured Cloudinary API secret key are used for signing the responses. + * + * The following method serves as a convenient utility to perform the verification procedure. + * + * @param publicId publicId response field value + * @param version version response field value + * @param signature signature response field value, i.e. the HMAC + * @return whether response signature is valid or not + */ + public boolean verifyApiResponseSignature(String publicId, String version, String signature) { + return new ApiResponseSignatureVerifier(config.apiSecret, config.signatureAlgorithm).verifySignature(publicId, version, signature); + } + + public void signRequest(Map params, Map options) { + String apiKey = ObjectUtils.asString(options.get("api_key"), this.config.apiKey); + if (apiKey == null) + throw new IllegalArgumentException("Must supply api_key"); + String apiSecret = ObjectUtils.asString(options.get("api_secret"), this.config.apiSecret); + if (apiSecret == null) + throw new IllegalArgumentException("Must supply api_secret"); + Util.clearEmpty(params); + params.put("signature", this.apiSignRequest(params, apiSecret, this.config.signatureVersion)); + params.put("api_key", apiKey); + } + + public String privateDownload(String publicId, String format, Map options) throws Exception { + Map params = new HashMap(); + params.put("public_id", publicId); + params.put("format", format); + params.put("attachment", options.get("attachment")); + params.put("type", options.get("type")); + params.put("expires_at", options.get("expires_at")); + params.put("timestamp", Util.timestamp()); + signRequest(params, options); + return buildUrl(cloudinaryApiUrl("download", options), params); + } + + public String zipDownload(String tag, Map options) throws Exception { + Map params = new HashMap(); + params.put("timestamp", Util.timestamp()); + params.put("tag", tag); + Object transformation = options.get("transformation"); + if (transformation != null) { + if (transformation instanceof Transformation) { + transformation = ((Transformation) transformation).generate(); + } + params.put("transformation", transformation.toString()); + } + params.put("transformation", transformation); + signRequest(params, options); + return buildUrl(cloudinaryApiUrl("download_tag.zip", options), params); + } + + public String downloadArchive(Map options, String targetFormat) throws UnsupportedEncodingException { + Map params = Util.buildArchiveParams(options, targetFormat); + params.put("mode", ArchiveParams.MODE_DOWNLOAD); + signRequest(params, options); + return buildUrl(cloudinaryApiUrl("generate_archive", options), params); + } + + public String downloadArchive(ArchiveParams params) throws UnsupportedEncodingException { + return downloadArchive(params.toMap(), params.targetFormat()); + } + + public String downloadZip(Map options) throws UnsupportedEncodingException { + return downloadArchive(options, "zip"); + } + + public String downloadGeneratedSprite(String tag, Map options) throws IOException { + if (StringUtils.isEmpty(tag)) throw new IllegalArgumentException("Tag cannot be empty"); + + if (options == null) + options = new HashMap(); + + options.put("tag", tag); + options.put("mode", ArchiveParams.MODE_DOWNLOAD); + + Map params = Util.buildGenerateSpriteParams(options); + signRequest(params, options); + + return buildUrl(cloudinaryApiUrl("sprite", options), params); + } + + public String downloadGeneratedSprite(String[] urls, Map options) throws IOException { + if (urls.length < 1) throw new IllegalArgumentException("Request must contain at least one URL."); + if (options == null) + options = new HashMap(); + + options.put("urls", urls); + options.put("mode", ArchiveParams.MODE_DOWNLOAD); + + Map params = Util.buildGenerateSpriteParams(options); + signRequest(params, options); + + return buildUrl(cloudinaryApiUrl("sprite", options), params); + } + + public String downloadMulti(String tag, Map options) throws IOException { + if (StringUtils.isEmpty(tag)) throw new IllegalArgumentException("Tag cannot be empty"); + if (options == null) + options = new HashMap(); + + options.put("tag", tag); + options.put("mode", ArchiveParams.MODE_DOWNLOAD); + + Map params = buildMultiParams(options); + signRequest(params, options); + + return buildUrl(cloudinaryApiUrl("multi", options), params); + } + + public String downloadMulti(String[] urls, Map options) throws IOException { + if (urls.length < 1) throw new IllegalArgumentException("Request must contain at least one URL."); + if (options == null) + options = new HashMap(); + + options.put("urls", urls); + options.put("mode", ArchiveParams.MODE_DOWNLOAD); + + Map params = buildMultiParams(options); + signRequest(params, options); + + return buildUrl(cloudinaryApiUrl("multi", options), params); + } + + /** + * Generates URL for executing "Download Folder" operation on Cloudinary site. + * + * @param folderPath path of folder to generate download URL for + * @param options optional, holds hints for URL generation procedure, see documentation for full list + * @return generated URL for downloading specified folder as ZIP archive + */ + public String downloadFolder(String folderPath, Map options) throws UnsupportedEncodingException { + if (StringUtils.isEmpty(folderPath)) { + throw new IllegalArgumentException("Folder path parameter value is required"); + } + + Map adjustedOptions = new HashMap(); + if (options != null) { + adjustedOptions.putAll(options); + } + + adjustedOptions.put("prefixes", folderPath); + + final Object resourceType = adjustedOptions.get("resource_type"); + adjustedOptions.put("resource_type", resourceType != null ? resourceType : "all"); + + return downloadArchive(adjustedOptions, (String) adjustedOptions.get("target_format")); + } + + /** + * Returns an URL of a specific version of a backed up asset that can be used to download that + * version of the asset (within an hour of the request). + * + * @param assetId The identifier of the uploaded asset. + * @param versionId The identifier of a backed up version of the asset. + * @param options Optional, holds hints for URL generation procedure, see documentation for + * full list + * @return The download URL of the asset + */ + public String downloadBackedupAsset(String assetId, String versionId, Map options) throws UnsupportedEncodingException { + if (StringUtils.isEmpty(assetId)) { + throw new IllegalArgumentException("AssetId parameter is required"); + } + + if (StringUtils.isEmpty(versionId)) { + throw new IllegalArgumentException("VersionId parameter is required"); + } + + Map params = new HashMap(); + params.put("asset_id", assetId); + params.put("version_id", versionId); + params.put("timestamp", Util.timestamp()); + + signRequest(params, options); + return buildUrl(cloudinaryApiUrl("download_backup", options), params); + } + + private String buildUrl(String base, Map params) throws UnsupportedEncodingException { + StringBuilder urlBuilder = new StringBuilder(); + urlBuilder.append(base); + if (!params.isEmpty()) { + urlBuilder.append("?"); + } + boolean first = true; + for (Map.Entry param : params.entrySet()) { + if (param.getValue() == null) continue; + + String keyValue = null; + Object value = param.getValue(); + if (!first) urlBuilder.append("&"); + if (value instanceof Object[]) + value = Arrays.asList(value); + if (value instanceof Collection) { + String key = param.getKey() + "[]="; + Collection items = (Collection) value; + List encodedItems = new ArrayList(); + for (Object item : items) + encodedItems.add(URLEncoder.encode(item.toString(), "UTF-8")); + keyValue = key + StringUtils.join(encodedItems, "&" + key); + } else { + keyValue = param.getKey() + "=" + + URLEncoder.encode(value.toString(), "UTF-8"); } + urlBuilder.append(keyValue); + first = false; } - return result; + return urlBuilder.toString(); } - } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Configuration.java b/cloudinary-core/src/main/java/com/cloudinary/Configuration.java new file mode 100644 index 00000000..7586ae46 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/Configuration.java @@ -0,0 +1,558 @@ +package com.cloudinary; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLDecoder; +import java.util.HashMap; +import java.util.Map; + +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +/** + * Configuration object for a {@link Cloudinary} instance + */ +public class Configuration { + public final static String CF_SHARED_CDN = "d3jpl91pxevbkh.cloudfront.net"; + public final static String OLD_AKAMAI_SHARED_CDN = "cloudinary-a.akamaihd.net"; + public final static String AKAMAI_SHARED_CDN = "res.cloudinary.com"; + public final static String SHARED_CDN = AKAMAI_SHARED_CDN; + public final static String VERSION = "1.0.2"; + public final static String USER_AGENT = "cld-android-" + VERSION; + public static final boolean DEFAULT_IS_LONG_SIGNATURE = false; + public static final SignatureAlgorithm DEFAULT_SIGNATURE_ALGORITHM = SignatureAlgorithm.SHA1; + public static final int DEFAULT_SIGNATURE_VERSION = 2; + + private static final String CONFIG_PROP_SIGNATURE_ALGORITHM = "signature_algorithm"; + + public String cloudName; + public String apiKey; + public String apiSecret; + public String secureDistribution; + public String cname; + public String uploadPrefix; + public boolean secure; + public boolean privateCdn; + public boolean cdnSubdomain; + public boolean shorten; + public String callback; + public String proxyHost; + public int proxyPort; + public Map properties = new HashMap(); + public Boolean secureCdnSubdomain; + public boolean useRootPath; + public boolean useFetchFormat; + public int timeout; + public boolean loadStrategies = true; + public boolean clientHints = false; + public AuthToken authToken; + public boolean forceVersion = true; + public boolean longUrlSignature = DEFAULT_IS_LONG_SIGNATURE; + public SignatureAlgorithm signatureAlgorithm = DEFAULT_SIGNATURE_ALGORITHM; + public int signatureVersion = DEFAULT_SIGNATURE_VERSION; + public String oauthToken = null; + public Boolean analytics; + public Configuration() { + } + + private Configuration( + String cloudName, + String apiKey, + String apiSecret, + String secureDistribution, + String cname, + String uploadPrefix, + boolean secure, + boolean privateCdn, + boolean cdnSubdomain, + boolean shorten, + String callback, + String proxyHost, + int proxyPort, + Boolean secureCdnSubdomain, + boolean useRootPath, + boolean useFetchFormat, + int timeout, + boolean loadStrategies, + boolean forceVersion, + boolean longUrlSignature, + SignatureAlgorithm signatureAlgorithm, + int signatureVersion, + String oauthToken, + boolean analytics) { + this.cloudName = cloudName; + this.apiKey = apiKey; + this.apiSecret = apiSecret; + this.secureDistribution = secureDistribution; + this.cname = cname; + this.uploadPrefix = uploadPrefix; + this.secure = secure; + this.privateCdn = privateCdn; + this.cdnSubdomain = cdnSubdomain; + this.shorten = shorten; + this.callback = callback; + this.proxyHost = proxyHost; + this.proxyPort = proxyPort; + this.secureCdnSubdomain = secureCdnSubdomain; + this.useRootPath = useRootPath; + this.useFetchFormat = useFetchFormat; + this.timeout = timeout; + this.loadStrategies = loadStrategies; + this.forceVersion = forceVersion; + this.longUrlSignature = longUrlSignature; + this.signatureAlgorithm = signatureAlgorithm; + this.signatureVersion = signatureVersion; + this.oauthToken = oauthToken; + this.analytics = analytics; + } + + @SuppressWarnings("rawtypes") + public Configuration(Map config) { + update(config); + } + + @SuppressWarnings("rawtypes") + public void update(Map config) { + this.cloudName = (String) config.get("cloud_name"); + this.apiKey = (String) config.get("api_key"); + this.apiSecret = (String) config.get("api_secret"); + this.secureDistribution = (String) config.get("secure_distribution"); + this.cname = (String) config.get("cname"); + this.secure = ObjectUtils.asBoolean(config.get("secure"), true); + this.privateCdn = ObjectUtils.asBoolean(config.get("private_cdn"), false); + this.cdnSubdomain = ObjectUtils.asBoolean(config.get("cdn_subdomain"), false); + this.shorten = ObjectUtils.asBoolean(config.get("shorten"), false); + this.uploadPrefix = (String) config.get("upload_prefix"); + this.callback = (String) config.get("callback"); + this.proxyHost = (String) config.get("proxy_host"); + this.proxyPort = ObjectUtils.asInteger(config.get("proxy_port"), 0); + this.secureCdnSubdomain = ObjectUtils.asBoolean(config.get("secure_cdn_subdomain"), null); + this.useRootPath = ObjectUtils.asBoolean(config.get("use_root_path"), false); + this.useFetchFormat = ObjectUtils.asBoolean(config.get("use_fetch_format"), false); + this.loadStrategies = ObjectUtils.asBoolean(config.get("load_strategies"), true); + this.timeout = ObjectUtils.asInteger(config.get("timeout"), 0); + this.clientHints = ObjectUtils.asBoolean(config.get("client_hints"), false); + this.analytics = ObjectUtils.asBoolean(config.get("analytics"), true); + Map tokenMap = (Map) config.get("auth_token"); + if (tokenMap != null) { + this.authToken = new AuthToken(tokenMap); + } + this.forceVersion = ObjectUtils.asBoolean(config.get("force_version"), true); + Map properties = (Map) config.get("properties"); + if (properties != null) { + this.properties.putAll(properties); + } + this.longUrlSignature = ObjectUtils.asBoolean(config.get("long_url_signature"), DEFAULT_IS_LONG_SIGNATURE); + this.signatureAlgorithm = SignatureAlgorithm.valueOf(ObjectUtils.asString(config.get(CONFIG_PROP_SIGNATURE_ALGORITHM), DEFAULT_SIGNATURE_ALGORITHM.name())); + this.signatureVersion = ObjectUtils.asInteger(config.get("signature_version"), DEFAULT_SIGNATURE_VERSION); + this.oauthToken = (String) config.get("oauth_token"); + + } + + @SuppressWarnings("rawtypes") + public Map asMap() { + Map map = new HashMap(); + map.put("cloud_name", cloudName); + map.put("api_key", apiKey); + map.put("api_secret", apiSecret); + map.put("secure_distribution", secureDistribution); + map.put("cname", cname); + map.put("secure", secure); + map.put("private_cdn", privateCdn); + map.put("cdn_subdomain", cdnSubdomain); + map.put("shorten", shorten); + map.put("upload_prefix", uploadPrefix); + map.put("callback", callback); + map.put("proxy_host", proxyHost); + map.put("proxy_port", proxyPort); + map.put("secure_cdn_subdomain", secureCdnSubdomain); + map.put("use_root_path", useRootPath); + map.put("use_fetch_format", useFetchFormat); + map.put("load_strategies", loadStrategies); + map.put("timeout", timeout); + map.put("client_hints", clientHints); + if (authToken != null) { + map.put("auth_token", authToken.asMap()); + } + map.put("force_version", forceVersion); + map.put("properties", new HashMap(properties)); + map.put("long_url_signature", longUrlSignature); + map.put(CONFIG_PROP_SIGNATURE_ALGORITHM, signatureAlgorithm.toString()); + map.put("signature_version", signatureVersion); + map.put("oauth_token", oauthToken); + map.put("analytics", analytics); + return map; + } + + + public Configuration(Configuration other) { + this.cloudName = other.cloudName; + this.apiKey = other.apiKey; + this.apiSecret = other.apiSecret; + this.secureDistribution = other.secureDistribution; + this.cname = other.cname; + this.uploadPrefix = other.uploadPrefix; + this.secure = other.secure; + this.privateCdn = other.privateCdn; + this.cdnSubdomain = other.cdnSubdomain; + this.shorten = other.shorten; + this.callback = other.callback; + this.proxyHost = other.proxyHost; + this.proxyPort = other.proxyPort; + this.secureCdnSubdomain = other.secureCdnSubdomain; + this.useRootPath = other.useRootPath; + this.useFetchFormat = other.useFetchFormat; + this.timeout = other.timeout; + this.clientHints = other.clientHints; + if (other.authToken != null) { + this.authToken = other.authToken.copy(); + } + this.forceVersion = other.forceVersion; + this.loadStrategies = other.loadStrategies; + this.properties.putAll(other.properties); + this.longUrlSignature = other.longUrlSignature; + this.signatureAlgorithm = other.signatureAlgorithm; + this.signatureVersion = other.signatureVersion; + this.oauthToken = other.oauthToken; + this.analytics = other.analytics; + } + + /** + * Create a new Configuration from an existing one + * + * @param other + * @return a new configuration with the arguments supplied by another configuration object + */ + public static Configuration from(Configuration other) { + return new Builder().from(other).build(); + } + + /** + * Create a Configuration from a cloudinary url + *

+ * Example url: cloudinary://123456789012345:abcdeghijklmnopqrstuvwxyz12@n07t21i7 + * + * @param cloudinaryUrl configuration url + * @return a new configuration with the arguments supplied by the url + */ + public static Configuration from(String cloudinaryUrl) { + Configuration config = new Configuration(); + config.update(parseConfigUrl(cloudinaryUrl)); + return config; + } + + + static protected Map parseConfigUrl(String cloudinaryUrl) { + Map params = new HashMap(); + URI cloudinaryUri = URI.create(cloudinaryUrl); + if (cloudinaryUri.getScheme() == null || !cloudinaryUri.getScheme().equalsIgnoreCase("cloudinary")){ + throw new IllegalArgumentException("Invalid CLOUDINARY_URL scheme. Expecting to start with 'cloudinary://'"); + } + params.put("cloud_name", cloudinaryUri.getHost()); + if (cloudinaryUri.getUserInfo() != null) { + String[] creds = cloudinaryUri.getUserInfo().split(":"); + params.put("api_key", creds[0]); + if (creds.length > 1) { + params.put("api_secret", creds[1]); + } + } + params.put("private_cdn", !StringUtils.isEmpty(cloudinaryUri.getPath())); + params.put("secure_distribution", cloudinaryUri.getPath()); + updateMapfromURI(params, cloudinaryUri); + return params; + } + + static private void updateMapfromURI(Map params, URI cloudinaryUri) { + if (cloudinaryUri.getQuery() != null) { + for (String param : cloudinaryUri.getQuery().split("&")) { + String[] keyValue = param.split("="); + try { + final String value = URLDecoder.decode(keyValue[1], "ASCII"); + final String key = keyValue[0]; + if(isNestedKey(key)) { + putNestedValue(params, key, value); + } else { + params.put(key, value); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException("Unexpected exception", e); + } + } + } + } + + static private void putNestedValue(Map params, String key, String value) { + String[] chain = key.split("[\\[\\]]+"); + Map outer = params; + String innerKey = chain[0]; + for (int i = 0; i < chain.length -1; i++, innerKey = chain[i]) { + Map inner = (Map) outer.get(innerKey); + if (inner == null) { + inner = new HashMap(); + outer.put(innerKey, inner); + } + outer = inner; + } + outer.put(innerKey, value); + } + + static private boolean isNestedKey(String key) { + return key.matches("\\w+\\[\\w+\\]"); + } + + /** + * Build a new {@link Configuration} + */ + public static class Builder { + private String cloudName; + private String apiKey; + private String apiSecret; + private String secureDistribution; + private String cname; + private String uploadPrefix; + private boolean secure; + private boolean privateCdn; + private boolean cdnSubdomain; + private boolean shorten; + private String callback; + private String proxyHost; + private int proxyPort; + private Boolean secureCdnSubdomain; + private boolean useRootPath; + private boolean useFetchFormat; + private boolean loadStrategies = true; + private int timeout; + private boolean clientHints = false; + private AuthToken authToken; + private boolean forceVersion = true; + private boolean longUrlSignature = DEFAULT_IS_LONG_SIGNATURE; + private SignatureAlgorithm signatureAlgorithm = DEFAULT_SIGNATURE_ALGORITHM; + private int signatureVersion = DEFAULT_SIGNATURE_VERSION; + private String oauthToken = null; + private boolean analytics; + + /** + * Set the HTTP connection timeout. + * + * @param timeout time in seconds, or 0 to use the default platform value + * @return builder for chaining + */ + public Builder setTimeout(int timeout) { + this.timeout = timeout; + return this; + } + + /** + * Creates a {@link Configuration} with the arguments supplied to this builder + */ + public Configuration build() { + final Configuration configuration = new Configuration( + cloudName, + apiKey, + apiSecret, + secureDistribution, + cname, + uploadPrefix, + secure, + privateCdn, + cdnSubdomain, + shorten, + callback, + proxyHost, + proxyPort, + secureCdnSubdomain, + useRootPath, + useFetchFormat, + timeout, + loadStrategies, + forceVersion, + longUrlSignature, + signatureAlgorithm, + signatureVersion, + oauthToken, + analytics); + configuration.clientHints = clientHints; + return configuration; + } + + /** + * The unique name of your cloud at Cloudinary + * You can find your cloud name in the Account Details section in the dashboard of Cloudinary Management Console. + */ + public Builder setCloudName(String cloudName) { + this.cloudName = cloudName; + return this; + } + + /** + * API Key + * You can find API Key in the Account Details section in the dashboard of Cloudinary Management Console. + */ + public Builder setApiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + + /** + * API Secret + * You can find API Secret in the Account Details section in the dashboard of Cloudinary Management Console. + */ + public Builder setApiSecret(String apiSecret) { + this.apiSecret = apiSecret; + return this; + } + + /** + * The domain name of the CDN distribution to use for building HTTPS URLs. + * Relevant only for Advanced plan's users that have a private CDN distribution. + */ + public Builder setSecureDistribution(String secureDistribution) { + this.secureDistribution = secureDistribution; + return this; + } + + /** + * Custom domain name to use for building HTTP URLs. + * Relevant only for Advanced plan's users that have a private CDN distribution and a custom CNAME. + */ + public Builder setCname(String cname) { + this.cname = cname; + return this; + } + + /** + * Force HTTPS URLs of images even if embedded in non-secure HTTP pages. + */ + public Builder setSecure(boolean secure) { + this.secure = secure; + return this; + } + + /** + * Should be set to true for Advanced plan's users that have a private CDN distribution. + */ + public Builder setPrivateCdn(boolean privateCdn) { + this.privateCdn = privateCdn; + return this; + } + + public Builder setSecureCdnSubdomain(Boolean secureCdnSubdomain) { + this.secureCdnSubdomain = secureCdnSubdomain; + return this; + } + + + /** + * Whether to automatically build URLs with multiple CDN sub-domains. + */ + public Builder setCdnSubdomain(boolean cdnSubdomain) { + this.cdnSubdomain = cdnSubdomain; + return this; + } + + public Builder setShorten(boolean shorten) { + this.shorten = shorten; + return this; + } + + public Builder setCallback(String callback) { + this.callback = callback; + return this; + } + + public Builder setUploadPrefix(String uploadPrefix) { + this.uploadPrefix = uploadPrefix; + return this; + } + + public Builder setUseRootPath(boolean useRootPath) { + this.useRootPath = useRootPath; + return this; + } + + public Builder setUseFetchFormat(boolean useFetchFormat) { + this.useFetchFormat = useFetchFormat; + return this; + } + + public Builder setLoadStrategies(boolean loadStrategies) { + this.loadStrategies = loadStrategies; + return this; + } + + public Builder setAnalytics(boolean analytics) { + this.analytics = analytics; + return this; + } + + public Builder setClientHints(boolean clientHints) { + this.clientHints = clientHints; + return this; + } + + public Builder setAuthToken(AuthToken authToken) { + this.authToken = authToken; + return this; + } + public Builder setForceVersion(boolean forceVersion) { + this.forceVersion = forceVersion; + return this; + } + + public Builder setIsLongUrlSignature(boolean isLong) { + this.longUrlSignature = isLong; + return this; + } + + public Builder setSignatureAlgorithm(SignatureAlgorithm signatureAlgorithm) { + this.signatureAlgorithm = signatureAlgorithm; + return this; + } + + public Builder setSignatureVersion(int signatureVersion) { + this.signatureVersion = signatureVersion; + return this; + } + + public Builder setOAuthToken(String oauthToken) { + this.oauthToken = oauthToken; + return this; + } + + /** + * Initialize builder from existing {@link Configuration} + * + * @param other a different configuration object + * @return an initialized builder configured with other + */ + public Builder from(Configuration other) { + this.cloudName = other.cloudName; + this.apiKey = other.apiKey; + this.apiSecret = other.apiSecret; + this.secureDistribution = other.secureDistribution; + this.cname = other.cname; + this.uploadPrefix = other.uploadPrefix; + this.secure = other.secure; + this.privateCdn = other.privateCdn; + this.cdnSubdomain = other.cdnSubdomain; + this.shorten = other.shorten; + this.callback = other.callback; + this.proxyHost = other.proxyHost; + this.proxyPort = other.proxyPort; + this.secureCdnSubdomain = other.secureCdnSubdomain; + this.useRootPath = other.useRootPath; + this.useFetchFormat = other.useFetchFormat; + this.loadStrategies = other.loadStrategies; + this.timeout = other.timeout; + this.clientHints = other.clientHints; + this.authToken = other.authToken == null ? null : other.authToken.copy(); + this.forceVersion = other.forceVersion; + this.longUrlSignature = other.longUrlSignature; + this.signatureAlgorithm = other.signatureAlgorithm; + this.signatureVersion = other.signatureVersion; + this.oauthToken = other.oauthToken; + this.analytics = other.analytics; + return this; + } + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/Coordinates.java b/cloudinary-core/src/main/java/com/cloudinary/Coordinates.java index 70bb3fec..2c025e6d 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Coordinates.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Coordinates.java @@ -1,83 +1,81 @@ package com.cloudinary; -import java.util.Collection; +import java.io.Serializable; import java.util.ArrayList; +import java.util.Collection; + +import com.cloudinary.utils.Rectangle; +import com.cloudinary.utils.StringUtils; + +public class Coordinates implements Serializable{ + + Collection coordinates = new ArrayList(); + + public Coordinates() { + } + + public Coordinates(Collection coordinates) { + this.coordinates = coordinates; + } -import org.apache.commons.lang.StringUtils; + public Coordinates(int[] rect) { + Collection coordinates = new ArrayList(); + if (rect.length != 4) { + throw new IllegalArgumentException("Must supply exactly 4 values for coordinates (x,y,width,height)"); + } + coordinates.add(new Rectangle(rect[0], rect[1], rect[2], rect[3])); + this.coordinates = coordinates; + } -import java.awt.Rectangle; -public class Coordinates { + public Coordinates(Rectangle rect) { + Collection coordinates = new ArrayList(); + coordinates.add(rect); + this.coordinates = coordinates; + } - Collection coordinates = new ArrayList(); - - public Coordinates() { - } + public Coordinates(String stringCoords) throws IllegalArgumentException { + Collection coordinates = new ArrayList(); + for (String stringRect : stringCoords.split("\\|")) { + if (StringUtils.isEmpty(stringRect)) + continue; + String[] elements = stringRect.split(","); + if (elements.length != 4) { + throw new IllegalArgumentException(String.format("Must supply exactly 4 values for coordinates (x,y,width,height) %d supplied: %s", + elements.length, stringRect)); + } + coordinates.add(new Rectangle(Integer.parseInt(elements[0]), Integer.parseInt(elements[1]), Integer.parseInt(elements[2]), Integer + .parseInt(elements[3]))); + } + this.coordinates = coordinates; + } - public Coordinates(Collection coordinates) { - this.coordinates = coordinates; - } - - public Coordinates(int[] rect) { - Collection coordinates = new ArrayList(); - if (rect.length != 4) { - throw new IllegalArgumentException("Must supply exactly 4 values for coordinates (x,y,width,height)"); - } - coordinates.add(new Rectangle(rect[0], rect[1], rect[2], rect[3])); - this.coordinates = coordinates; - } - - public Coordinates(Rectangle rect) { - Collection coordinates = new ArrayList(); - coordinates.add(rect); - this.coordinates = coordinates; - } - - public Coordinates(String stringCoords) throws IllegalArgumentException { - Collection coordinates = new ArrayList(); - for (String stringRect : stringCoords.split("\\|")) { - if (stringRect.isEmpty()) continue; - String[] elements = stringRect.split(","); - if (elements.length != 4) { - throw new IllegalArgumentException(String.format("Must supply exactly 4 values for coordinates (x,y,width,height) %d supplied: %s", elements.length, stringRect)); - } - coordinates.add(new Rectangle( - Integer.parseInt(elements[0]), - Integer.parseInt(elements[1]), - Integer.parseInt(elements[2]), - Integer.parseInt(elements[3]))); - } - this.coordinates = coordinates; - } - - - public static Coordinates parseCoordinates(Object coordinates) throws IllegalArgumentException { - if (coordinates instanceof Coordinates) { - return (Coordinates)coordinates; - } else if (coordinates instanceof int[]) { - return new Coordinates((int[]) coordinates); - } else if (coordinates instanceof Rectangle) { - return new Coordinates((Rectangle) coordinates); - } else { - return new Coordinates(coordinates.toString()); - } - } + public static Coordinates parseCoordinates(Object coordinates) throws IllegalArgumentException { + if (coordinates instanceof Coordinates) { + return (Coordinates) coordinates; + } else if (coordinates instanceof int[]) { + return new Coordinates((int[]) coordinates); + } else if (coordinates instanceof Rectangle) { + return new Coordinates((Rectangle) coordinates); + } else { + return new Coordinates(coordinates.toString()); + } + } - public void addRect(Rectangle rect) { - this.coordinates.add(rect); - } + public void addRect(Rectangle rect) { + this.coordinates.add(rect); + } - - public Collection underlaying() { - return this.coordinates; - } + public Collection underlaying() { + return this.coordinates; + } - @Override - public String toString() { - ArrayList rects = new ArrayList(); - for (Rectangle rect : this.coordinates) { - rects.add(rect.x + "," + rect.y + "," + rect.width + "," + rect.height); - } - return StringUtils.join(rects, "|"); - } + @Override + public String toString() { + ArrayList rects = new ArrayList(); + for (Rectangle rect : this.coordinates) { + rects.add(rect.x + "," + rect.y + "," + rect.width + "," + rect.height); + } + return StringUtils.join(rects, "|"); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/CustomFunction.java b/cloudinary-core/src/main/java/com/cloudinary/CustomFunction.java new file mode 100644 index 00000000..b2a13d32 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/CustomFunction.java @@ -0,0 +1,31 @@ +package com.cloudinary; + +import com.cloudinary.utils.Base64Coder; + +/** + * Helper class to generate a custom function params to be used in {@link Transformation#customFunction(CustomFunction)}. + */ +public class CustomFunction extends BaseParam{ + + private CustomFunction(String... components) { + super(components); + } + + /** + * Generate a web-assembly custom action param to send to {@link Transformation#customFunction(CustomFunction)} + * @param publicId The public id of the web-assembly file + * @return A new instance of custom action param + */ + public static CustomFunction wasm(String publicId){ + return new CustomFunction("wasm", publicId); + } + + /** + * Generate a remote lambda custom action param to send to {@link Transformation#customFunction(CustomFunction)} + * @param url The public url of the aws lambda function + * @return A new instance of custom action param + */ + public static CustomFunction remote(String url){ + return new CustomFunction("remote", Base64Coder.encodeURLSafeString(url)); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/EagerTransformation.java b/cloudinary-core/src/main/java/com/cloudinary/EagerTransformation.java index bfd9c0e1..fc34ee0f 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/EagerTransformation.java +++ b/cloudinary-core/src/main/java/com/cloudinary/EagerTransformation.java @@ -1,24 +1,57 @@ package com.cloudinary; +import com.cloudinary.utils.StringUtils; + +import java.util.ArrayList; import java.util.List; import java.util.Map; -public class EagerTransformation extends Transformation { - protected String format; - public EagerTransformation(List transformations) { - super(transformations); - } - - public EagerTransformation() { - super(); - } - - public EagerTransformation format(String format) { - this.format = format; - return this; - } - - public String getFormat() { - return format; - } +public class EagerTransformation extends Transformation { + protected String format; + + @SuppressWarnings("rawtypes") + public EagerTransformation(List transformations) { + super(transformations); + } + + public EagerTransformation() { + super(); + } + + public EagerTransformation format(String format) { + this.format = format; + return this; + } + + public String getFormat() { + return format; + } + + @Override + public String generate(Iterable optionsList) { + List components = new ArrayList(); + for (Map options : optionsList) { + if (options.size() > 0) { + components.add(super.generate(options)); + } + } + + if (format != null){ + components.add(format); + } + + return StringUtils.join(components, "/"); + } + + @Override + public String generate(Map options) { + List eager = new ArrayList(); + eager.add(super.generate(options)); + + if (format != null){ + eager.add(format); + } + + return StringUtils.join(eager, "/"); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/ProgressCallback.java b/cloudinary-core/src/main/java/com/cloudinary/ProgressCallback.java new file mode 100644 index 00000000..7ef81b01 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/ProgressCallback.java @@ -0,0 +1,13 @@ +package com.cloudinary; + +/** + * Defines a callback for network operations. + */ +public interface ProgressCallback { + /** + * Invoked during network operation. + * @param bytesUploaded the number of bytes uploaded so far + * @param totalBytes the total number of byte to upload - if known + */ + void onProgress(long bytesUploaded, long totalBytes); +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/ResponsiveBreakpoint.java b/cloudinary-core/src/main/java/com/cloudinary/ResponsiveBreakpoint.java new file mode 100644 index 00000000..d377f5b7 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/ResponsiveBreakpoint.java @@ -0,0 +1,72 @@ +package com.cloudinary; + +import org.cloudinary.json.JSONObject; + +public class ResponsiveBreakpoint extends JSONObject { + public ResponsiveBreakpoint() { + put("create_derived", true); + } + + public boolean isCreateDerived() { + return optBoolean("create_derived"); + } + + public ResponsiveBreakpoint createDerived(boolean createDerived) { + put("create_derived", createDerived); + return this; + } + + public Transformation transformation() { + return (Transformation) opt("transformation"); + } + + public ResponsiveBreakpoint transformation(Transformation transformation) { + put("transformation", transformation); + return this; + } + + public ResponsiveBreakpoint format(String format) { + put("format", format); + return this; + } + + public String format() { + return optString("format"); + } + + public int maxWidth() { + return optInt("max_width"); + } + + public ResponsiveBreakpoint maxWidth(int maxWidth) { + put("max_width", maxWidth); + return this; + } + + public int minWidth() { + return optInt("min_width"); + } + + public ResponsiveBreakpoint minWidth(Integer minWidth) { + put("min_width", minWidth); + return this; + } + + public int bytesStep() { + return optInt("bytes_step"); + } + + public ResponsiveBreakpoint bytesStep(Integer bytesStep) { + put("bytes_step", bytesStep); + return this; + } + + public int maxImages() { + return optInt("max_images"); + } + + public ResponsiveBreakpoint maxImages(Integer maxImages) { + put("max_images", maxImages); + return this; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/Search.java b/cloudinary-core/src/main/java/com/cloudinary/Search.java new file mode 100644 index 00000000..369830c6 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/Search.java @@ -0,0 +1,139 @@ +package com.cloudinary; + +import com.cloudinary.api.ApiResponse; +import com.cloudinary.utils.Base64Coder; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.cloudinary.json.JSONObject; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public class Search { + + protected final Api api; + private ArrayList> sortByParam; + private ArrayList aggregateParam; + private ArrayList withFieldParam; + private HashMap params; + private ArrayList fields; + + private int ttl = 300; + + Search(Cloudinary cloudinary) { + this.api = cloudinary.api(); + this.params = new HashMap(); + this.sortByParam = new ArrayList>(); + this.aggregateParam = new ArrayList(); + this.withFieldParam = new ArrayList(); + this.fields = new ArrayList(); + } + + public Search ttl(int ttl) { + this.ttl = ttl; + return this; + } + public Search expression(String value) { + this.params.put("expression", value); + return this; + } + + public Search maxResults(Integer value) { + this.params.put("max_results", value); + return this; + } + + public Search nextCursor(String value) { + this.params.put("next_cursor", value); + return this; + } + + public Search aggregate(String field) { + if (!aggregateParam.contains(field)) { + aggregateParam.add(field); + } + return this; + } + + public Search withField(String field) { + if (!withFieldParam.contains(field)) { + withFieldParam.add(field); + } + return this; + } + + public Search sortBy(String field, String dir) { + HashMap sortBucket = new HashMap(); + sortBucket.put(field, dir); + for (int i = 0; i < sortByParam.size(); i++) { + if (sortByParam.get(i).containsKey(field)){ + sortByParam.add(i, sortBucket); + return this; + } + } + sortByParam.add(sortBucket); + return this; + } + + public Search fields(String field) { + if (!fields.contains(field)) { + fields.add(field); + } + return this; + } + + public HashMap toQuery() { + HashMap queryParams = new HashMap(this.params); + if (withFieldParam.size() > 0) { + queryParams.put("with_field", withFieldParam); + } + if(sortByParam.size() > 0) { + queryParams.put("sort_by", sortByParam); + } + if(aggregateParam.size() > 0) { + queryParams.put("aggregate", aggregateParam); + } + if(fields.size() > 0) { + queryParams.put("fields", fields); + } + return queryParams; + } + + public ApiResponse execute() throws Exception { + Map options = ObjectUtils.asMap("content_type", "json"); + return this.api.callApi(Api.HttpMethod.POST, Arrays.asList("resources", "search"), this.toQuery(), options); + } + + + public String toUrl() throws Exception { + return toUrl(null, null); + } + + public String toUrl(String nextCursor) throws Exception { + return toUrl(null, nextCursor); + } + /*** + Creates a signed Search URL that can be used on the client side. + ***/ + public String toUrl(Integer ttl, String nextCursor) throws Exception { + String nextCursorParam = nextCursor; + String apiSecret = api.cloudinary.config.apiSecret; + if (apiSecret == null) throw new IllegalArgumentException("Must supply api_secret"); + if(ttl == null) { + ttl = this.ttl; + } + HashMap queryParams = toQuery(); + if(nextCursorParam == null) { + nextCursorParam = (String) queryParams.get("next_cursor"); + } + queryParams.remove("next_cursor"); + JSONObject json = ObjectUtils.toJSON(queryParams); + String base64Query = Base64Coder.encodeURLSafeString(json.toString()); + String signature = StringUtils.encodeHexString(Util.hash(String.format("%d%s%s", ttl, base64Query, apiSecret), SignatureAlgorithm.SHA256)); + String prefix = Url.unsignedDownloadUrlPrefix(null,api.cloudinary.config); + + return String.format("%s/search/%s/%d/%s%s", prefix, signature, ttl, base64Query,nextCursorParam != null && !nextCursorParam.isEmpty() ? "/"+nextCursorParam : ""); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/SearchFolders.java b/cloudinary-core/src/main/java/com/cloudinary/SearchFolders.java new file mode 100644 index 00000000..1e8bc5bd --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/SearchFolders.java @@ -0,0 +1,19 @@ +package com.cloudinary; + +import com.cloudinary.api.ApiResponse; +import com.cloudinary.utils.ObjectUtils; + +import java.util.Arrays; +import java.util.Map; + +public class SearchFolders extends Search { + + public SearchFolders(Cloudinary cloudinary) { + super(cloudinary); + } + + public ApiResponse execute() throws Exception { + Map options = ObjectUtils.asMap("content_type", "json"); + return this.api.callApi(Api.HttpMethod.POST, Arrays.asList("folders", "search"), this.toQuery(), options); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/SignatureAlgorithm.java b/cloudinary-core/src/main/java/com/cloudinary/SignatureAlgorithm.java new file mode 100644 index 00000000..c96fb1b3 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/SignatureAlgorithm.java @@ -0,0 +1,19 @@ +package com.cloudinary; + +/** + * Defines supported algorithms for generating/verifying hashed message authentication codes (HMAC). + */ +public enum SignatureAlgorithm { + SHA1("SHA-1"), + SHA256("SHA-256"); + + private final String algorithmId; + + SignatureAlgorithm(String algorithmId) { + this.algorithmId = algorithmId; + } + + public String getAlgorithmId() { + return algorithmId; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/SmartUrlEncoder.java b/cloudinary-core/src/main/java/com/cloudinary/SmartUrlEncoder.java index 8c72f9fd..2f20414f 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/SmartUrlEncoder.java +++ b/cloudinary-core/src/main/java/com/cloudinary/SmartUrlEncoder.java @@ -3,12 +3,14 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; -public class SmartUrlEncoder { - public static String encode(String input) { - try { - return URLEncoder.encode(input, "UTF-8").replace("%2F", "/").replace("%3A", ":").replace("+", "%20"); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - } +public final class SmartUrlEncoder { + private SmartUrlEncoder() {} + + public static String encode(String input) { + try { + return URLEncoder.encode(input, "UTF-8").replace("%2F", "/").replace("%3A", ":").replace("+", "%20"); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/StoredFile.java b/cloudinary-core/src/main/java/com/cloudinary/StoredFile.java index 739c15e5..04e5ccca 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/StoredFile.java +++ b/cloudinary-core/src/main/java/com/cloudinary/StoredFile.java @@ -20,6 +20,8 @@ public class StoredFile { private static final String IMAGE_RESOURCE_TYPE = "image"; + private static final String VIDEO_RESOURCE_TYPE = "video"; + private static final String AUTO_RESOURCE_TYPE = "auto"; private static final Pattern PRELOADED_PATTERN = Pattern.compile("^([^\\/]+)\\/([^\\/]+)\\/v(\\d+)\\/([^#]+)#?([^\\/]+)?$"); @@ -78,10 +80,7 @@ public void setType(String type) { public String getPreloadedFile() { StringBuilder sb = new StringBuilder(); - sb.append(resourceType).append("/") - .append(type) - .append("/v").append(version).append("/") - .append(publicId); + sb.append(resourceType).append("/").append(type).append("/v").append(version).append("/").append(publicId); if (format != null && !format.isEmpty()) { sb.append(".").append(format); } @@ -99,7 +98,8 @@ public void setPreloadedFile(String uri) { type = match.group(2); version = Long.parseLong(match.group(3)); String filename = match.group(4); - if (match.groupCount() == 5) signature = match.group(5); + if (match.groupCount() == 5) + signature = match.group(5); int lastDotIndex = filename.lastIndexOf('.'); if (lastDotIndex == -1) { publicId = filename; @@ -121,4 +121,8 @@ public String getComputedSignature(Cloudinary cloudinary) { public boolean getIsImage() { return IMAGE_RESOURCE_TYPE.equals(resourceType) || AUTO_RESOURCE_TYPE.equals(resourceType); } + + public boolean getIsVideo() { + return VIDEO_RESOURCE_TYPE.equals(resourceType); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Transformation.java b/cloudinary-core/src/main/java/com/cloudinary/Transformation.java index 76d11a78..c4b2ca9e 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Transformation.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Transformation.java @@ -1,366 +1,991 @@ package com.cloudinary; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.SortedMap; -import java.util.TreeMap; - -import org.apache.commons.collections.CollectionUtils; -import org.apache.commons.collections.Predicate; -import org.apache.commons.collections.Transformer; -import org.apache.commons.lang.StringUtils; - -@SuppressWarnings({ "rawtypes", "unchecked" }) -public class Transformation { - protected Map transformation; - protected List transformations; - protected String htmlWidth; - protected String htmlHeight; - protected boolean hiDPI = false; - protected boolean isResponsive = false; - protected static boolean defaultIsResponsive = false; - protected static Object defaultDPR = null; - - private static final Map DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = Cloudinary.asMap("width", "auto", "crop", - "limit"); - protected static Map responsiveWidthTransformation = null; - - public Transformation(Transformation transformation) { - this(dup(transformation.transformations)); - this.hiDPI = transformation.isHiDPI(); - this.isResponsive = transformation.isResponsive(); - } - - // Warning: options will destructively updated! - public Transformation(List transformations) { - this.transformations = transformations; - if (transformations.isEmpty()) { - chain(); - } else { - this.transformation = transformations.get(transformations.size() - 1); - } - } - - public Transformation() { - this.transformations = new ArrayList(); - chain(); - } - - public Transformation width(Object value) { - return param("width", value); - } - - public Transformation height(Object value) { - return param("height", value); - } - - public Transformation named(String... value) { - return param("transformation", value); - } - - public Transformation crop(String value) { - return param("crop", value); - } - - public Transformation background(String value) { - return param("background", value); - } - - public Transformation color(String value) { - return param("color", value); - } - - public Transformation effect(String value) { - return param("effect", value); - } - - public Transformation effect(String effect, Object param) { - return param("effect", effect + ":" + param); - } - - public Transformation angle(int value) { - return param("angle", value); - } - - public Transformation angle(String... value) { - return param("angle", value); - } - - public Transformation border(String value) { - return param("border", value); - } - - public Transformation border(int width, String color) { - return param("border", "" + width + "px_solid_" + color.replaceFirst("^#", "rgb:")); - } - - public Transformation x(Object value) { - return param("x", value); - } - - public Transformation y(Object value) { - return param("y", value); - } - - public Transformation radius(Object value) { - return param("radius", value); - } - - public Transformation quality(Object value) { - return param("quality", value); - } - - public Transformation defaultImage(String value) { - return param("default_image", value); - } - - public Transformation gravity(String value) { - return param("gravity", value); - } - - public Transformation colorSpace(String value) { - return param("color_space", value); - } - - public Transformation prefix(String value) { - return param("prefix", value); - } - - public Transformation overlay(String value) { - return param("overlay", value); - } - - public Transformation underlay(String value) { - return param("underlay", value); - } - - public Transformation fetchFormat(String value) { - return param("fetch_format", value); - } - - public Transformation density(Object value) { - return param("density", value); - } - - public Transformation page(Object value) { - return param("page", value); - } - - public Transformation delay(Object value) { - return param("delay", value); - } - - public Transformation opacity(int value) { - return param("opacity", value); - } - - public Transformation rawTransformation(String value) { - return param("raw_transformation", value); - } - - public Transformation flags(String... value) { - return param("flags", value); - } - - public Transformation dpr(float value) { - return param("dpr", value); - } - - public Transformation dpr(int value) { - return param("dpr", value); - } - - public Transformation dpr(String value) { - return param("dpr", value); - } - - public Transformation responsiveWidth(boolean value) { - return param("responsive_width", value); - } - - public boolean isResponsive() { - return this.isResponsive; - } - - public boolean isHiDPI() { - return this.hiDPI; - } - - // Warning: options will destructively updated! - public Transformation params(Map transformation) { - this.transformation = transformation; - transformations.add(transformation); - return this; - } - - public Transformation chain() { - return params(new HashMap()); - } - - public Transformation param(String key, Object value) { - transformation.put(key, value); - return this; - } - - public String generate() { - return generate(transformations); - } - - public String generate(Iterable optionsList) { - List components = new ArrayList(); - for (Map options : optionsList) { - components.add(generate(options)); - } - return StringUtils.join(components, "/"); - } - - public String generate(Map options) { - boolean isResponsive = Cloudinary.asBoolean(options.get("responsive_width"), defaultIsResponsive); - - String size = (String) options.get("size"); - if (size != null) { - String[] size_components = size.split("x"); - options.put("width", size_components[0]); - options.put("height", size_components[1]); - } - String width = this.htmlWidth = Cloudinary.asString(options.get("width")); - String height = this.htmlHeight = Cloudinary.asString(options.get("height")); - boolean hasLayer = StringUtils.isNotBlank((String) options.get("overlay")) - || StringUtils.isNotBlank((String) options.get("underlay")); - - String crop = (String) options.get("crop"); - String angle = StringUtils.join(Cloudinary.asArray(options.get("angle")), "."); - - boolean noHtmlSizes = hasLayer || StringUtils.isNotBlank(angle) || "fit".equals(crop) || "limit".equals(crop); - if (width != null && (width.equals("auto") || Float.parseFloat(width) < 1 || noHtmlSizes || isResponsive)) { - this.htmlWidth = null; - } - if (height != null && (Float.parseFloat(height) < 1 || noHtmlSizes || isResponsive)) { - this.htmlHeight = null; - } - - String background = (String) options.get("background"); - if (background != null) { - background = background.replaceFirst("^#", "rgb:"); - } - - String color = (String) options.get("color"); - if (color != null) { - color = color.replaceFirst("^#", "rgb:"); - } - - List transformations = Cloudinary.asArray(options.get("transformation")); - Predicate isAMap = new Predicate() { - public boolean evaluate(Object value) { - return value instanceof Map; - } - }; - String namedTransformation = null; - if (CollectionUtils.exists(transformations, isAMap)) { - CollectionUtils.transform(transformations, new Transformer() { - public Object transform(Object baseTransformation) { - if (baseTransformation instanceof Map) { - return generate((Map) baseTransformation); - } else { - Map map = new HashMap(); - map.put("transformation", baseTransformation); - return generate(map); - } - } - }); - } else { - namedTransformation = StringUtils.join(transformations, "."); - transformations = new ArrayList(); - } - - String flags = StringUtils.join(Cloudinary.asArray(options.get("flags")), "."); - - SortedMap params = new TreeMap(); - params.put("w", width); - params.put("h", height); - params.put("t", namedTransformation); - params.put("c", crop); - params.put("b", background); - params.put("co", color); - params.put("a", angle); - params.put("fl", flags); - String dpr = Cloudinary.asString(options.get("dpr"), null == defaultDPR ? null : defaultDPR.toString()); - params.put("dpr", dpr); - - String[] simple_params = new String[] { "x", "x", "y", "y", "r", "radius", "d", "default_image", "g", - "gravity", "cs", "color_space", "p", "prefix", "l", "overlay", "u", "underlay", "f", "fetch_format", - "dn", "density", "pg", "page", "dl", "delay", "e", "effect", "bo", "border", "q", "quality", "o", - "opacity" }; - for (int i = 0; i < simple_params.length; i += 2) { - params.put(simple_params[i], Cloudinary.asString(options.get(simple_params[i + 1]))); - } - List components = new ArrayList(); - for (Map.Entry param : params.entrySet()) { - if (StringUtils.isNotBlank(param.getValue())) { - components.add(param.getKey() + "_" + param.getValue()); - } - } - String raw_transformation = (String) options.get("raw_transformation"); - if (raw_transformation != null) { - components.add(raw_transformation); - } - if (!components.isEmpty()) { - transformations.add(StringUtils.join(components, ",")); - } - - if (isResponsive) { - transformations.add(generate(getResponsiveWidthTransformation())); - } - - if ("auto".equals(width) || isResponsive) { - this.isResponsive = true; - } - - if ("auto".equals(dpr)) { - this.hiDPI = true; - } - - return StringUtils.join(transformations, "/"); - } - - public String getHtmlWidth() { - return htmlWidth; - } - - public String getHtmlHeight() { - return htmlHeight; - } - - private static List dup(List transformations) { - List result = new ArrayList(); - for (Map params : transformations) { - result.add(new HashMap(params)); - } - return result; - } - - public static void setResponsiveWidthTransformation(Map transformation) { - responsiveWidthTransformation = transformation; - } - - private static Map getResponsiveWidthTransformation() { - Map result = new HashMap(); - if (null == responsiveWidthTransformation) { - result.putAll(DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION); - } else { - result.putAll(responsiveWidthTransformation); - } - return result; - } - - public static void setDefaultIsResponsive(boolean isResponsive) { - defaultIsResponsive = isResponsive; - } - - public static void setDefaultDPR(Object dpr) { - defaultDPR = dpr; - } - +import java.io.Serializable; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.cloudinary.transformation.AbstractLayer; +import com.cloudinary.transformation.Condition; +import com.cloudinary.transformation.Expression; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +@SuppressWarnings({"rawtypes", "unchecked"}) +public class Transformation implements Serializable { + public static final String VAR_NAME_RE = "^\\$[a-zA-Z][a-zA-Z0-9]+$"; + protected Map transformation; + protected List transformations; + protected String htmlWidth; + protected String htmlHeight; + protected boolean hiDPI = false; + protected boolean isResponsive = false; + protected static boolean defaultIsResponsive = false; + protected static Object defaultDPR = null; + + private static final Map DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION = ObjectUtils.asMap("width", "auto", "crop", "limit"); + protected static Map responsiveWidthTransformation = null; + private static final Pattern RANGE_VALUE_RE = Pattern.compile("^((?:\\d+\\.)?\\d+)([%pP])?$"); + private static final Pattern RANGE_RE = Pattern.compile("^(\\d+\\.)?\\d+[%pP]?\\.\\.(\\d+\\.)?\\d+[%pP]?$"); + private static final String[] SIMPLE_PARAMS = new String[]{ + "ac", "audio_codec", + "af", "audio_frequency", + "bo", "border", + "br", "bit_rate", + "cs", "color_space", + "d", "default_image", + "dl", "delay", + "dn", "density", + "f", "fetch_format", + "fn", "custom_function", + "fps", "fps", + "g", "gravity", + "l", "overlay", + "p", "prefix", + "pg", "page", + "u", "underlay", + "vs", "video_sampling", + "sp", "streaming_profile", + "ki", "keyframe_interval" + }; + + public Transformation(Transformation transformation) { + this(dup(transformation.transformations)); + this.hiDPI = transformation.isHiDPI(); + this.isResponsive = transformation.isResponsive(); + } + + // Warning: options will destructively updated! + public Transformation(List transformations) { + this.transformations = transformations; + if (transformations.isEmpty()) { + chain(); + } else { + this.transformation = transformations.get(transformations.size() - 1); + } + } + + public Transformation() { + this.transformations = new ArrayList(); + chain(); + } + + public T width(Object value) { + return param("width", value); + } + + public T height(Object value) { + return param("height", value); + } + + public T named(String... value) { + return param("transformation", value); + } + + public T crop(String value) { + return param("crop", value); + } + + public T background(String value) { + return param("background", value); + } + + public T color(String value) { + return param("color", value); + } + + public T effect(String value) { + return param("effect", value); + } + + public T effect(String effect, Object param) { + return param("effect", effect + ":" + param); + } + + public T angle(int value) { + return param("angle", value); + } + + public T angle(String... value) { + return param("angle", value); + } + + public T border(String value) { + return param("border", value); + } + + public T border(int width, String color) { + return param("border", "" + width + "px_solid_" + replaceColorPrefix(color)); + } + + public T x(Object value) { + return param("x", value); + } + + public T y(Object value) { + return param("y", value); + } + + /** + * Add rounding transformation. + *

+ * Radius can be specified either as value in pixels or expression. Specify 0 to keep corner untouched. + * + * @param value rounding radius for all four corners + * @return updated transformation instance for chaining + */ + public T radius(Object value) { + return radius(new Object[]{value}); + } + + /** + * Add rounding transformation. + *

+ * Radius can be specified either as value in pixels or expression. Specify 0 to keep corner untouched. + * + * @param topLeftBottomRight rounding radius for top-left and bottom-right corners + * @param topRightBottomLeft rounding radius for top-right and bottom-left corners + * @return updated transformation instance for chaining + */ + public T radius(Object topLeftBottomRight, Object topRightBottomLeft) { + return radius(new Object[]{topLeftBottomRight, topRightBottomLeft}); + } + + /** + * Add rounding transformation. + *

+ * Radius can be specified either as value in pixels or expression. Specify 0 to keep corner untouched. + * + * @param topLeft rounding radius for top-left corner + * @param topRightBottomLeft rounding radius for top-right and bottom-left corners + * @param bottomRight rounding radius for bottom-right corner + * @return updated transformation instance for chaining + */ + public T radius(Object topLeft, Object topRightBottomLeft, Object bottomRight) { + return radius(new Object[]{topLeft, topRightBottomLeft, bottomRight}); + } + + /** + * Add rounding transformation. + *

+ * Radius can be specified either as value in pixels or expression. Specify 0 to keep corner untouched. + * + * @param topLeft rounding radius for top-left corner + * @param topRight rounding radius for top-right corner + * @param bottomRight rounding radius for bottom-right corner + * @param bottomLeft rounding radius for bottom-left corner + * @return updated transformation instance for chaining + */ + public T radius(Object topLeft, Object topRight, Object bottomRight, Object bottomLeft) { + return radius(new Object[]{topLeft, topRight, bottomRight, bottomLeft}); + } + + /** + * Add rounding transformation. + *

+ * Radius can be specified either as value in pixels or expression. Specify 0 to keep corner untouched. + * + * @param cornerRadiuses rounding radiuses for corners as array + * @return updated transformation instance for chaining + */ + public T radius(Object[] cornerRadiuses) { + return param("radius", cornerRadiuses); + } + + public T quality(Object value) { + return param("quality", value); + } + + public T defaultImage(String value) { + return param("default_image", value); + } + + public T gravity(String value) { + return param("gravity", value); + } + + /** + * Set the keyframe interval parameter + * + * @param value Interval in seconds + * @return The transformation for chaining + */ + public T keyframeInterval(float value) { + return param("keyframe_interval", value); + } + + /** + * Set the keyframe interval parameter + * + * @param value Interval in seconds. + * @return The transformation for chaining + */ + public T keyframeInterval(String value) { + return param("keyframe_interval", value); + } + + public T colorSpace(String value) { + return param("color_space", value); + } + + public T prefix(String value) { + return param("prefix", value); + } + + public T overlay(String value) { + return param("overlay", value); + } + + public T overlay(AbstractLayer value) { + return param("overlay", value); + } + + public T underlay(String value) { + return param("underlay", value); + } + + public T underlay(AbstractLayer value) { + return param("underlay", value); + } + + public T fetchFormat(String value) { + return param("fetch_format", value); + } + + public T density(Object value) { + return param("density", value); + } + + public T page(Object value) { + return param("page", value); + } + + public T delay(Object value) { + return param("delay", value); + } + + public T opacity(Object value) { + return param("opacity", value); + } + + public T rawTransformation(String value) { + return param("raw_transformation", value); + } + + public T flags(String... value) { + return param("flags", value); + } + + public T dpr(float value) { + return param("dpr", value); + } + + public T dpr(int value) { + return param("dpr", value); + } + + public T dpr(String value) { + return param("dpr", value); + } + + public T duration(String value) { + return param("duration", value); + } + + public T duration(float value) { + return param("duration", new Float(value)); + } + + public T duration(double value) { + return param("duration", new Double(value)); + } + + public T durationPercent(float value) { + return param("duration", new Float(value).toString() + "p"); + } + + public T durationPercent(double value) { + return param("duration", new Double(value).toString() + "p"); + } + + public T startOffset(String value) { + return param("start_offset", value); + } + + public T startOffset(float value) { + return param("start_offset", new Float(value)); + } + + public T startOffset(double value) { + return param("start_offset", new Double(value)); + } + + public T startOffsetPercent(float value) { + return param("start_offset", new Float(value).toString() + "p"); + } + + public T startOffsetPercent(double value) { + return param("start_offset", new Double(value).toString() + "p"); + } + + public T endOffset(String value) { + return param("end_offset", value); + } + + public T endOffset(float value) { + return param("end_offset", new Float(value)); + } + + public T endOffset(double value) { + return param("end_offset", new Double(value)); + } + + public T endOffsetPercent(float value) { + return param("end_offset", new Float(value).toString() + "p"); + } + + public T endOffsetPercent(double value) { + return param("end_offset", new Double(value).toString() + "p"); + } + + public T offset(String value) { + return param("offset", value); + } + + public T offset(String[] value) { + if (value.length < 2) throw new IllegalArgumentException("Offset range must include at least 2 items"); + return param("offset", value); + } + + public T offset(float[] value) { + if (value.length < 2) throw new IllegalArgumentException("Offset range must include at least 2 items"); + Number[] numberArray = new Number[]{value[0], value[1]}; + return offset(numberArray); + } + + public T offset(double[] value) { + if (value.length < 2) throw new IllegalArgumentException("Offset range must include at least 2 items"); + Number[] numberArray = new Number[]{value[0], value[1]}; + return offset(numberArray); + } + + public T offset(Number[] value) { + if (value.length < 2) throw new IllegalArgumentException("Offset range must include at least 2 items"); + return param("offset", value); + } + + public T videoCodec(String value) { + return param("video_codec", value); + } + + public T videoCodec(Map value) { + return param("video_codec", value); + } + + public T audioCodec(String value) { + return param("audio_codec", value); + } + + public T audioFrequency(String value) { + return param("audio_frequency", value); + } + + public T audioFrequency(int value) { + return param("audio_frequency", value); + } + + public T bitRate(String value) { + return param("bit_rate", value); + } + + public T bitRate(int value) { + return param("bit_rate", new Integer(value)); + } + + public T videoSampling(String value) { + return param("video_sampling", value); + } + + public T videoSamplingFrames(int value) { + return param("video_sampling", value); + } + + public T videoSamplingSeconds(Number value) { + return param("video_sampling", value.toString() + "s"); + } + + public T videoSamplingSeconds(int value) { + return videoSamplingSeconds(new Integer(value)); + } + + public T videoSamplingSeconds(float value) { + return videoSamplingSeconds(new Float(value)); + } + + public T videoSamplingSeconds(double value) { + return videoSamplingSeconds(new Double(value)); + } + + public T zoom(String value) { + return param("zoom", value); + } + + public T zoom(float value) { + return param("zoom", new Float(value)); + } + + public T zoom(double value) { + return param("zoom", new Double(value)); + } + + public T aspectRatio(double value) { + return param("aspect_ratio", new Double(value)); + } + + public T aspectRatio(String value) { + return param("aspect_ratio", value); + } + + public T aspectRatio(int nom, int denom) { + return aspectRatio(Integer.toString(nom) + ":" + Integer.toString(denom)); + } + + public T responsiveWidth(boolean value) { + return param("responsive_width", value); + } + + /** + * Start defining a condition, which will be completed with a call {@link Condition#then()} + * + * @return condition + */ + public Condition ifCondition() { + return new Condition().setParent(this); + } + + /** + * Define a conditional transformation defined by the condition string + * + * @param condition a condition string + * @return the transformation for chaining + */ + public T ifCondition(String condition) { + return param("if", condition); + } + + + /** + * Define a conditional transformation + * + * @param expression a condition + * @return the transformation for chaining + */ + public T ifCondition(Expression expression) { + return ifCondition(expression.toString()); + } + + /** + * Define a conditional transformation + * + * @param condition a condition + * @return the transformation for chaining + */ + public T ifCondition(Condition condition) { + return ifCondition(condition.toString()); + } + + public T ifElse() { + chain(); + return param("if", "else"); + } + + public T endIf() { + chain(); + int transSize = this.transformations.size(); + for (int i = transSize - 1; i >= 0; i--) { + Map segment = this.transformations.get(i); // [..., {if: "w_gt_1000",c: "fill", w: 500}, ...] + Object value = segment.get("if"); + if (value != null) { // if: "w_gt_1000" + String ifValue = value.toString(); + if (ifValue.equals("end")) break; + if (segment.size() > 1) { + segment.remove("if"); // {c: fill, w: 500} + transformations.set(i, segment); // [..., {c: fill, w: 500}, ...] + transformations.add(i, ObjectUtils.asMap("if", value)); // // [..., "if_w_gt_1000", {c: fill, w: 500}, ...] + } + if (!"else".equals(ifValue)) break; // otherwise keep looking for if_condition + } + } + + param("if", "end"); + return chain(); + } + + /** + * fps (frames per second) parameter for video + * + * @param value Either a single value int or float or a range in the format <start>[-<end>].
+ * For example, 23-29.7 + * @return the transformation for chaining + */ + public T fps(String value) { + return param("fps", value); + } + + /** + * fps (frames per second) parameter for video + * + * @param value the desired fps + * @return the transformation for chaining + */ + public T fps(double value) { + return param("fps", new Float(value)); + } + + /** + * fps (frames per second) parameter for video + * + * @param value the desired fps + * @return the transformation for chaining + */ + public T fps(int value) { + return param("fps", new Integer(value)); + } + + /** + * fps (frames per second) parameter for video + * @param rangeStart String or Number, can be null for open range. + * @param rangeEnd String or Number, can be null for open range. + * @return the transformation for chaining. + */ + public T fps(Object rangeStart, Object rangeEnd){ + if (rangeEnd == null && rangeStart == null){ + throw new IllegalArgumentException("At least one of [rangeStart, rangeEnd] must be provided"); + } + StringBuilder builder = new StringBuilder(); + if (rangeStart != null){ + builder.append(rangeStart); + } + + builder.append("-"); + + if (rangeEnd != null){ + builder.append(rangeEnd); + } + + return param("fps", builder.toString()); + } + + public T streamingProfile(String value) { + return param("streaming_profile", value); + } + + public boolean isResponsive() { + return this.isResponsive; + } + + public boolean isHiDPI() { + return this.hiDPI; + } + + // Warning: options will destructively updated! + public T params(Map transformation) { + this.transformation = transformation; + transformations.add(transformation); + return (T) this; + } + + public T chain() { + return params(new HashMap()); + } + + public T chainWith(Transformation transformation) { + List transformations = dup(this.transformations); + transformations.addAll(dup(transformation.transformations)); + return (T) new Transformation(transformations); + } + + public T param(String key, Object value) { + transformation.put(key, value); + return (T) this; + } + + /** + * Serialize this transformation object as a string + *

+ * {@code + * Transformation().width(100).height(101).generate(); // produces "h_101,w_100" + * } + * + * @return a String representation of the transformation + */ + public String generate() { + return generate(transformations); + } + + @Override + public String toString() { + return generate(); + } + + public String generate(Iterable optionsList) { + List components = new ArrayList(); + for (Map options : optionsList) { + if (options.size() > 0) { + components.add(generate(options)); + } + } + return StringUtils.join(components, "/"); + } + + public String generate(Map options) { + boolean isResponsive = ObjectUtils.asBoolean(options.get("responsive_width"), defaultIsResponsive); + + String size = (String) options.get("size"); + if (size != null) { + String[] size_components = size.split("x"); + options.put("width", size_components[0]); + options.put("height", size_components[1]); + } + String width = this.htmlWidth = ObjectUtils.asString(options.get("width")); + String height = this.htmlHeight = ObjectUtils.asString(options.get("height")); + boolean hasLayer = options.get("overlay") != null && StringUtils.isNotBlank(options.get("overlay").toString()) + || options.get("underlay") != null && StringUtils.isNotBlank(options.get("underlay").toString()); + + String crop = (String) options.get("crop"); + String angle = StringUtils.join(ObjectUtils.asArray(options.get("angle")), "."); + + boolean noHtmlSizes = hasLayer || StringUtils.isNotBlank(angle) || "fit".equals(crop) || "limit".equals(crop); + if (width != null && (width.startsWith("auto") || !isValidAttrValue(width) || noHtmlSizes || isResponsive)) { + this.htmlWidth = null; + } + if (height != null && (!isValidAttrValue(height) || noHtmlSizes || isResponsive)) { + this.htmlHeight = null; + } + + String background = (String) options.get("background"); + if (background != null) { + background = replaceColorPrefix(background); + } + + String color = (String) options.get("color"); + if (color != null) { + color = replaceColorPrefix(color); + } + + List transformations = ObjectUtils.asArray(options.get("transformation")); + boolean allNamed = true; + for ( int i =0; i < transformations.size(); i++ ){ + Object baseTransformation = transformations.get(i); + if (baseTransformation instanceof Map) { + allNamed = false; + break; + } else if (baseTransformation instanceof String){ + transformations.set(i, ((String) baseTransformation).replaceAll(" ", "%20")); + } + } + String namedTransformation = null; + if (allNamed) { + namedTransformation = StringUtils.join(transformations, "."); + transformations = new ArrayList(); + } else { + List ts = transformations; + transformations = new ArrayList(); + for (Object baseTransformation : ts) { + String transformationString; + if (baseTransformation instanceof Map) { + transformationString = generate((Map) baseTransformation); + } else { + Map map = new HashMap(); + map.put("transformation", baseTransformation); + transformationString = generate(map); + } + transformations.add(transformationString); + } + } + + + String flags = StringUtils.join(ObjectUtils.asArray(options.get("flags")), "."); + + String duration = normRangeValue(options.get("duration")); + String startOffset = normAutoRangeValue(options.get("start_offset")); + String endOffset = normRangeValue(options.get("end_offset")); + String[] offset = splitRange(options.get("offset")); + if (offset != null) { + startOffset = normAutoRangeValue(offset[0]); + endOffset = normRangeValue(offset[1]); + } + + String videoCodec = processVideoCodecParam(options.get("video_codec")); + String dpr = ObjectUtils.asString(options.get("dpr"), null == defaultDPR ? null : defaultDPR.toString()); + + + List components = new ArrayList(); + + String ifValue = (String) options.get("if"); + if (ifValue != null) { + components.add(0, "if_" + Expression.normalize(ifValue)); + } + + SortedSet varParams = new TreeSet(); + for (Object k : options.keySet()) { + String key = (String) k; + if (StringUtils.isVariable(key)) { + varParams.add(key + "_" + ObjectUtils.asString(options.get(k))); + } + } + + if (!varParams.isEmpty()) { + components.add(StringUtils.join(varParams, ",")); + } + + String variables = processVar((Expression[]) options.get("variables")); + if (variables != null) { + components.add(variables); + } + + Map params = new HashMap(64); + + params.put("a", Expression.normalize(angle)); + params.put("ar", Expression.normalize(options.get("aspect_ratio"))); + params.put("b", background); + params.put("c", crop); + params.put("co", color); + params.put("dpr", Expression.normalize(dpr)); + params.put("du", duration); + params.put("e", Expression.normalize(options.get("effect"))); + params.put("eo", endOffset); + params.put("fl", flags); + params.put("h", Expression.normalize(height)); + params.put("o", Expression.normalize(options.get("opacity"))); + params.put("q", Expression.normalize(options.get("quality"))); + params.put("r", Expression.normalize(radiusToExpression((Object[]) options.get("radius")))); + params.put("so", startOffset); + params.put("t", namedTransformation); + params.put("vc", videoCodec); + params.put("w", Expression.normalize(width)); + params.put("x", Expression.normalize(options.get("x"))); + params.put("y", Expression.normalize(options.get("y"))); + params.put("z", Expression.normalize(options.get("zoom"))); + + for (int i = 0; i < SIMPLE_PARAMS.length; i += 2) { + params.put(SIMPLE_PARAMS[i], ObjectUtils.asString(options.get(SIMPLE_PARAMS[i + 1]))); + } + + params = new TreeMap(params); + + for (Map.Entry param : params.entrySet()) { + if (StringUtils.isNotBlank(param.getValue())) { + components.add(param.getKey() + "_" + param.getValue()); + } + } + String raw_transformation = (String) options.get("raw_transformation"); + if (raw_transformation != null) { + components.add(raw_transformation); + } + if (!components.isEmpty()) { + final String joined = StringUtils.join(components, ","); + transformations.add(joined); + } + + if (isResponsive) { + transformations.add(generate(getResponsiveWidthTransformation())); + } + + if ("auto".equals(width) || isResponsive) { + this.isResponsive = true; + } + + if ("auto".equals(dpr)) { + this.hiDPI = true; + } + + return StringUtils.join(transformations, "/"); + } + + private String replaceColorPrefix(String color) { + return StringUtils.replaceIfFirstChar(color, '#', "rgb:"); + } + + private String processVar(Expression[] variables) { + if (variables == null) { + return null; + } + List s = new ArrayList(variables.length); + for (Expression variable : variables) { + s.add(variable.toString()); + } + return StringUtils.join(s, ","); + } + + /** + * Check if the value is a float >= 1 + * + * @param value + * @return true if the value is a float >= 1 + */ + private boolean isValidAttrValue(String value) { + final float parseFloat; + try { + parseFloat = Float.parseFloat(value); + } catch (NumberFormatException e) { + return false; + } + return parseFloat >= 1; + } + + public String getHtmlWidth() { + return htmlWidth; + } + + public String getHtmlHeight() { + return htmlHeight; + } + + private static List dup(List transformations) { + List result = new ArrayList(); + for (Map params : transformations) { + result.add(new HashMap(params)); + } + return result; + } + + public static void setResponsiveWidthTransformation(Map transformation) { + responsiveWidthTransformation = transformation; + } + + private static Map getResponsiveWidthTransformation() { + Map result = new HashMap(); + if (null == responsiveWidthTransformation) { + result.putAll(DEFAULT_RESPONSIVE_WIDTH_TRANSFORMATION); + } else { + result.putAll(responsiveWidthTransformation); + } + return result; + } + + public static void setDefaultIsResponsive(boolean isResponsive) { + defaultIsResponsive = isResponsive; + } + + public static void setDefaultDPR(Object dpr) { + defaultDPR = dpr; + } + + private static String[] splitRange(Object range) { + if (range instanceof String[] && ((String[]) range).length >= 2) { + String[] stringArrayRange = ((String[]) range); + return new String[]{stringArrayRange[0], stringArrayRange[1]}; + } else if (range instanceof Number[] && ((Number[]) range).length >= 2) { + Number[] numberArrayRange = ((Number[]) range); + return new String[]{numberArrayRange[0].toString(), numberArrayRange[1].toString()}; + } else if (range instanceof String && RANGE_RE.matcher((String) range).matches()) { + return ((String) range).split("\\.\\.", 2); + } else { + return null; + } + } + + private static String normRangeValue(Object objectValue) { + if (objectValue == null) return null; + String value = objectValue.toString(); + if (StringUtils.isEmpty(value)) return null; + + Matcher matcher = RANGE_VALUE_RE.matcher(value); + + if (!matcher.matches()) { + return Expression.normalize(value); + } + + String modifier = ""; + if (matcher.groupCount() == 2 && !StringUtils.isEmpty(matcher.group(2))) { + modifier = "p"; + } + return matcher.group(1) + modifier; + } + + private static String normAutoRangeValue(Object objectValue) { + if ("auto".equals(objectValue)) { + return objectValue.toString(); + } + return normRangeValue(objectValue); + } + + private static String processVideoCodecParam(Object param) { + StringBuilder outParam = new StringBuilder(); + if (param instanceof String) { + outParam.append(param); + } + if (param instanceof Map) { + Map paramMap = (Map) param; + outParam.append(paramMap.get("codec")); + if (paramMap.containsKey("profile")) { + outParam.append(":").append(paramMap.get("profile")); + if (paramMap.containsKey("level")) { + outParam.append(":").append(paramMap.get("level")); + if (paramMap.containsKey("b_frames") && paramMap.get("b_frames") == "false") { + outParam.append(":").append("bframes_no"); + } + } + } + } + return outParam.toString(); + } + + /** + * Add a variable assignment. Each call to this method will add a new variable assignments, but the order of the assignments may change. To enforce a particular order, use {@link #variables(Expression...)} + * + * @param name the name of the variable + * @param value the value to assign to the variable + * @return this for chaining + */ + public T variable(String name, Object value) { + return param(name, value); + } + + /** + * Add a sequence of variable assignments. The order of the assignments will be honored. + * + * @param variables variable expressions + * @return this for chaining + */ + public T variables(Expression... variables) { + return param("variables", variables); + } + + /** + * Set a custom action, such as a call to a lambda function or a web-assembly function. + * @param action The custom action to perform, see {@link CustomFunction}. + * @return The transformation for chaining + */ + public T customFunction(CustomFunction action) { + return param("custom_function", action.toString()); + } + + /** + * Set a custom pre-function, such as a call to a lambda function or a web-assembly function. + * @param action The custom action to perform, see {@link CustomFunction}. + * @return The transformation for chaining + */ + public T customPreFunction(CustomFunction action) { + return param("custom_function", "pre:" + action.toString()); + } + + private String radiusToExpression(Object[] radiusOption) { + if (radiusOption == null) { + return null; + } + + if (radiusOption.length == 0 || radiusOption.length > 4) { + throw new IllegalArgumentException("Radius array should contain between 1 and 4 values"); + } + + for (Object o : radiusOption) { + if (o == null) { + throw new IllegalArgumentException("Radius options array should not contain nulls"); + } + } + + return StringUtils.join(radiusOption, ":"); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Uploader.java b/cloudinary-core/src/main/java/com/cloudinary/Uploader.java index 777a9f4e..39b21950 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Uploader.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Uploader.java @@ -1,416 +1,631 @@ package com.cloudinary; -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.FileInputStream; -import java.io.ByteArrayInputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -import org.apache.commons.lang.StringEscapeUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.http.HttpResponse; -import org.apache.http.client.HttpClient; -import org.apache.http.client.methods.HttpPost; -import org.apache.http.entity.mime.HttpMultipartMode; -import org.apache.http.entity.mime.MultipartEntity; -import org.apache.http.entity.mime.content.ByteArrayBody; -import org.apache.http.entity.mime.content.FileBody; -import org.apache.http.entity.mime.content.StringBody; -import org.apache.http.impl.client.DefaultHttpClient; -import org.apache.http.conn.ClientConnectionManager; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; -import org.json.simple.parser.ParseException; - -@SuppressWarnings({ "rawtypes", "unchecked" }) +import com.cloudinary.strategies.AbstractUploaderStrategy; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.cloudinary.json.JSONObject; + +import java.io.*; +import java.util.*; + +import static com.cloudinary.Util.buildGenerateSpriteParams; +import static com.cloudinary.Util.buildMultiParams; + +@SuppressWarnings({"rawtypes", "unchecked"}) public class Uploader { - private final Cloudinary cloudinary; - public Uploader(Cloudinary cloudinary) { - this.cloudinary = cloudinary; - } + public static final int BUFFER_SIZE = 20000000; - public Map buildUploadParams(Map options) { + private final class Command { + final static String add = "add"; + final static String remove = "remove"; + final static String replace = "replace"; + final static String removeAll = "remove_all"; + + private Command() { + } + } + + public Map callApi(String action, Map params, Map options, Object file) throws IOException { + return strategy.callApi(action, params, options, file, null); + } + + public Map callApi(String action, Map params, Map options, Object file, ProgressCallback progressCallback) throws IOException { + return strategy.callApi(action, params, options, file, progressCallback); + } + + private Cloudinary cloudinary; + private AbstractUploaderStrategy strategy; + + public Uploader(Cloudinary cloudinary, AbstractUploaderStrategy strategy) { + this.cloudinary = cloudinary; + this.strategy = strategy; + strategy.init(this); + } + + public Cloudinary cloudinary() { + return this.cloudinary; + } + + public Map buildUploadParams(Map options) { return Util.buildUploadParams(options); - } - - public Map unsignedUpload(Object file, String uploadPreset, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - HashMap nextOptions = new HashMap(options); - nextOptions.put("unsigned", true); - nextOptions.put("upload_preset", uploadPreset); - return upload(file, nextOptions); - } - - public Map upload(Object file, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = buildUploadParams(options); - return callApi("upload", params, options, file); - } - - public Map uploadLargeRaw(Object file, Map options) throws IOException { - return uploadLargeRaw(file, options, 20000000); - } - - public Map uploadLargeRaw(Object file, Map options, int bufferSize) throws IOException { - InputStream input; - if (file instanceof InputStream) { - input = (InputStream) file; - } else if (file instanceof File) { - input = new FileInputStream((File) file); - } else if (file instanceof byte[]) { - input = new ByteArrayInputStream((byte[]) file); - } else { - input = new FileInputStream(new File(file.toString())); - } - try { - Map result = uploadLargeRawParts(input, options, bufferSize); - return result; - } finally { - input.close(); - } - } - - private Map uploadLargeRawParts(InputStream input, Map options, int bufferSize) throws IOException { - Map params = Cloudinary.only(options, "public_id", "backup", "type"); - Map nextParams = new HashMap(); - nextParams.putAll(params); - Map sentParams = new HashMap(); - - Map sentOptions = new HashMap(); - sentOptions.putAll(options); - sentOptions.put("resource_type", "raw"); - - byte[] buffer = new byte[bufferSize]; - int bytesRead = 0; - int currentBufferSize = 0; - int partNumber = 1; - while ((bytesRead = input.read(buffer, currentBufferSize, bufferSize - currentBufferSize)) != -1) { - if (bytesRead + currentBufferSize == bufferSize) { - nextParams.put("part_number", Integer.toString(partNumber)); - sentParams.clear(); - sentParams.putAll(nextParams); - Map response = callApi("upload_large", sentParams, sentOptions, buffer); - if (partNumber == 1) { - nextParams.put("public_id", response.get("public_id")); - nextParams.put("upload_id", response.get("upload_id")); - } - currentBufferSize = 0; - partNumber++; - } else { - currentBufferSize += bytesRead; - } - } - byte[] finalBuffer = new byte[currentBufferSize]; - System.arraycopy(buffer, 0, finalBuffer, 0, currentBufferSize); - nextParams.put("final", true); - nextParams.put("part_number", Integer.toString(partNumber)); - return callApi("upload_large", nextParams, sentOptions, finalBuffer); - } - - - public Map destroy(String publicId, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - params.put("type", (String) options.get("type")); - params.put("public_id", publicId); - params.put("invalidate", Cloudinary.asBoolean(options.get("invalidate"), false).toString()); - return callApi("destroy", params, options, null); - } - - public Map rename(String fromPublicId, String toPublicId, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - params.put("type", (String) options.get("type")); - params.put("overwrite", Cloudinary.asBoolean(options.get("overwrite"), false).toString()); - params.put("from_public_id", fromPublicId); - params.put("to_public_id", toPublicId); - return callApi("rename", params, options, null); - } - - public Map explicit(String publicId, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - params.put("public_id", publicId); - params.put("callback", (String) options.get("callback")); - params.put("type", (String) options.get("type")); - params.put("eager", Util.buildEager((List) options.get("eager"))); - params.put("headers", Util.buildCustomHeaders(options.get("headers"))); - params.put("tags", StringUtils.join(Cloudinary.asArray(options.get("tags")), ",")); - if (options.get("face_coordinates") != null) { - params.put("face_coordinates", Coordinates.parseCoordinates(options.get("face_coordinates")).toString()); - } - if (options.get("custom_coordinates") != null) { - params.put("custom_coordinates", Coordinates.parseCoordinates(options.get("custom_coordinates")).toString()); - } - if (options.get("context") != null) { - params.put("context", Cloudinary.encodeMap(options.get("context"))); - } - return callApi("explicit", params, options, null); - } - - public Map generate_sprite(String tag, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - Object transParam = options.get("transformation"); - Transformation transformation = null; - if (transParam instanceof Transformation) { - transformation = new Transformation((Transformation) transParam); - } else if (transParam instanceof String) { - transformation = new Transformation().rawTransformation((String) transParam); - } else { - transformation = new Transformation(); - } - String format = (String) options.get("format"); - if (format != null) { - transformation.fetchFormat(format); - } - params.put("transformation", transformation.generate()); - params.put("tag", tag); - params.put("notification_url", (String) options.get("notification_url")); - params.put("async", Cloudinary.asBoolean(options.get("async"), false).toString()); - return callApi("sprite", params, options, null); - } - - public Map multi(String tag, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - Object transformation = options.get("transformation"); - if (transformation != null) { - if (transformation instanceof Transformation) { - transformation = ((Transformation) transformation).generate(); - } - params.put("transformation", transformation.toString()); - } - params.put("tag", tag); - params.put("notification_url", (String) options.get("notification_url")); - params.put("format", (String) options.get("format")); - params.put("async", Cloudinary.asBoolean(options.get("async"), false).toString()); - return callApi("multi", params, options, null); - } - - public Map explode(String public_id, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - Object transformation = options.get("transformation"); - if (transformation != null) { - if (transformation instanceof Transformation) { - transformation = ((Transformation) transformation).generate(); - } - params.put("transformation", transformation.toString()); - } - params.put("public_id", public_id); - params.put("notification_url", (String) options.get("notification_url")); - params.put("format", (String) options.get("format")); - return callApi("explode", params, options, null); - } - - // options may include 'exclusive' (boolean) which causes clearing this tag - // from all other resources - public Map addTag(String tag, String[] publicIds, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - boolean exclusive = Cloudinary.asBoolean(options.get("exclusive"), false); - String command = exclusive ? "set_exclusive" : "add"; - return callTagsApi(tag, command, publicIds, options); - } - - public Map removeTag(String tag, String[] publicIds, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - return callTagsApi(tag, "remove", publicIds, options); - } - - public Map replaceTag(String tag, String[] publicIds, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - return callTagsApi(tag, "replace", publicIds, options); - } - - public Map callTagsApi(String tag, String command, String[] publicIds, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - params.put("tag", tag); - params.put("command", command); - params.put("type", (String) options.get("type")); - params.put("public_ids", Arrays.asList(publicIds)); - return callApi("tags", params, options, null); - } - - private final static String[] TEXT_PARAMS = { "public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", - "font_style", "background", "opacity", "text_decoration" }; - - public Map text(String text, Map options) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - params.put("text", text); - for (String param : TEXT_PARAMS) { - params.put(param, Cloudinary.asString(options.get(param))); - } - return callApi("text", params, options, null); - } - - public void signRequestParams(Map params, Map options) { - params.put("timestamp", new Long(System.currentTimeMillis() / 1000L).toString()); - cloudinary.signRequest(params, options); } - - public Uploader withConnectionManager(ClientConnectionManager connectionManager) { - this.connectionManager = connectionManager; - return this; - } - - public Map callApi(String action, Map params, Map options, Object file) throws IOException { - if (options == null) options = Cloudinary.emptyMap(); - boolean returnError = Cloudinary.asBoolean(options.get("return_error"), false); - - if (options.get("unsigned") == null || Boolean.FALSE.equals(options.get("unsigned"))) { - signRequestParams(params, options); - } else { - Util.clearEmpty(params); - } - - String apiUrl = cloudinary.cloudinaryApiUrl(action, options); - - HttpClient client = new DefaultHttpClient(connectionManager); - - HttpPost postMethod = new HttpPost(apiUrl); - postMethod.setHeader("User-Agent", Cloudinary.USER_AGENT); - - MultipartEntity multipart = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE); - // Remove blank parameters - for (Map.Entry param : params.entrySet()) { - if (param.getValue() instanceof Collection) { - for (Object value : (Collection) param.getValue()) { - multipart.addPart(param.getKey()+"[]", new StringBody(Cloudinary.asString(value))); - } - } else { - String value = param.getValue().toString(); - if (StringUtils.isNotBlank(value)) { - multipart.addPart(param.getKey(), new StringBody(value)); - } - } - } - - if (file instanceof String && !((String) file).matches("https?:.*|s3:.*|data:[^;]*;base64,([a-zA-Z0-9/+\n=]+)")) { - file = new File((String) file); - } - if (file instanceof File) { - multipart.addPart("file", new FileBody((File) file)); - } else if (file instanceof String) { - multipart.addPart("file", new StringBody((String) file)); + + public Map unsignedUpload(Object file, String uploadPreset, Map options) throws IOException { + return unsignedUpload(file, uploadPreset, options, null); + } + + public Map unsignedUpload(Object file, String uploadPreset, Map options, ProgressCallback progressCallback) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + HashMap nextOptions = new HashMap(options); + nextOptions.put("unsigned", true); + nextOptions.put("upload_preset", uploadPreset); + return upload(file, nextOptions, progressCallback); + } + + public Map upload(Object file, Map options) throws IOException { + return upload(file, options, null); + } + + public Map upload(Object file, Map options, final ProgressCallback progressCallback) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = buildUploadParams(options); + + return callApi("upload", params, options, file, progressCallback); + } + + public Map uploadLargeRaw(Object file, Map options) throws IOException { + return uploadLargeRaw(file, options, BUFFER_SIZE, null); + } + + public Map uploadLargeRaw(Object file, Map options, ProgressCallback progressCallback) throws IOException { + return uploadLargeRaw(file, options, BUFFER_SIZE, progressCallback); + } + + public Map uploadLargeRaw(Object file, Map options, int bufferSize) throws IOException { + return uploadLargeRaw(file, options, bufferSize, null); + } + + public Map uploadLargeRaw(Object file, Map options, int bufferSize, ProgressCallback callback) throws IOException { + Map sentOptions = new HashMap(); + sentOptions.putAll(options); + sentOptions.put("resource_type", "raw"); + return uploadLarge(file, sentOptions, bufferSize, callback); + } + + public Map uploadLarge(Object file, Map options) throws IOException { + return uploadLarge(file, options, null); + } + + public Map uploadLarge(Object file, Map options, ProgressCallback progressCallback) throws IOException { + int bufferSize = ObjectUtils.asInteger(options.get("chunk_size"), BUFFER_SIZE); + return uploadLarge(file, options, bufferSize, progressCallback); + } + + @SuppressWarnings("resource") + public Map uploadLarge(Object file, Map options, int bufferSize) throws IOException { + return uploadLarge(file, options, bufferSize, null); + } + + public Map uploadLarge(Object file, Map options, int bufferSize, ProgressCallback progressCallback) throws IOException { + return uploadLarge(file, options, bufferSize, 0, null, progressCallback); + } + + public Map uploadLarge(Object file, Map options, int bufferSize, long offset, String uniqueUploadId, ProgressCallback progressCallback) throws IOException { + InputStream input; + long length = -1; + boolean remote = false; + String filename = null; + if (file instanceof InputStream) { + input = (InputStream) file; + } else if (file instanceof File) { + length = ((File) file).length(); + filename = ((File) file).getName(); + input = new FileInputStream((File) file); } else if (file instanceof byte[]) { - multipart.addPart("file", new ByteArrayBody((byte[]) file, "file")); - } else if (file == null) { - // no-problem - } else { - throw new IOException("Uprecognized file parameter " + file); - } - postMethod.setEntity(multipart); - - HttpResponse response = client.execute(postMethod); - int code = response.getStatusLine().getStatusCode(); - InputStream responseStream = response.getEntity().getContent(); - String responseData = readFully(responseStream); - - if (code != 200 && code != 400 && code != 500) { - throw new RuntimeException("Server returned unexpected status code - " + code + " - " + responseData); - } - - Map result; - try { - result = (Map) JSONValue.parseWithException(responseData); - } catch (ParseException e) { - throw new RuntimeException("Invalid JSON response from server " + e.getMessage()); - } - if (result.containsKey("error")) { - Map error = (Map) result.get("error"); - if (returnError) { - error.put("http_code", code); - } else { - throw new RuntimeException((String) error.get("message")); - } - } - return result; - } - - public String uploadTagParams(Map options) { - if (options == null) options = new HashMap(); - if (options.get("resource_type") == null) { - options = new HashMap(options); - options.put("resource_type", "auto"); - } - - String callback = Cloudinary.asString(options.get("callback"), this.cloudinary.getStringConfig("callback")); - if (callback == null) { - throw new IllegalArgumentException("Must supply callback"); - } - options.put("callback", callback); - - Map params = this.buildUploadParams(options); - if (options.get("unsigned") == null || Boolean.FALSE.equals(options.get("unsigned"))) { - signRequestParams(params, options); - } else { - Util.clearEmpty(params); - } - - return JSONObject.toJSONString(params); - } - - public String getUploadUrl(Map options) { - if (options == null) options = new HashMap(); - return this.cloudinary.cloudinaryApiUrl("upload", options); - } - - public String unsignedImageUploadTag(String field, String uploadPreset, Map options, Map htmlOptions) { - Map nextOptions = new HashMap(options); - nextOptions.put("upload_preset", uploadPreset); - nextOptions.put("unsigned", true); - return imageUploadTag(field, nextOptions, htmlOptions); - } - - public String imageUploadTag(String field, Map options, Map htmlOptions) { - if (htmlOptions == null) htmlOptions = Cloudinary.emptyMap(); - - String tagParams = StringEscapeUtils.escapeHtml(uploadTagParams(options)); - - String cloudinaryUploadUrl = getUploadUrl(options); - - StringBuilder builder = new StringBuilder(); - builder.append(""); - return builder.toString(); - } - - protected static String readFully(InputStream in) throws IOException { - ByteArrayOutputStream baos = new ByteArrayOutputStream(); - byte[] buffer = new byte[1024]; - int length = 0; - while ((length = in.read(buffer)) != -1) { - baos.write(buffer, 0, length); - } - return new String(baos.toByteArray()); - } - - private ClientConnectionManager connectionManager = null; + length = ((byte[]) file).length; + input = new ByteArrayInputStream((byte[]) file); + } else { + if (StringUtils.isRemoteUrl(file.toString())){ + remote = true; + input = null; + } else { + File f = new File(file.toString()); + length = f.length(); + filename = f.getName(); + input = new FileInputStream(f); + } + } + try { + final Map result; + if (remote) { + result = upload(file, options); + } else { + if (!options.containsKey("filename") && StringUtils.isNotBlank(filename)) { + options.put("filename", filename); + } + result = uploadLargeParts(input, options, bufferSize, length, offset, uniqueUploadId, progressCallback); + } + return result; + } finally { + if (input != null) { + input.close(); + } + } + } + + private Map uploadLargeParts(InputStream input, Map options, int bufferSize, long length, long offset, String uniqueUploadId, final ProgressCallback progressCallback) throws IOException { + Map params = buildUploadParams(options); + + Map sentOptions = new HashMap(); + sentOptions.putAll(options); + Map extraHeaders = new HashMap(); + extraHeaders.put("X-Unique-Upload-Id", StringUtils.isBlank(uniqueUploadId) ? cloudinary().randomPublicId() : uniqueUploadId); + sentOptions.put("extra_headers", extraHeaders); + + byte[] buffer = new byte[bufferSize]; + byte[] nibbleBuffer = new byte[1]; + int bytesRead = 0; + int currentBufferSize = 0; + int partNumber = 0; + long totalBytes = offset; + Map response = null; + final long knownLengthBeforeUpload = length; + long totalBytesUploaded = offset; + input.skip(offset); + while (true) { + bytesRead = input.read(buffer, currentBufferSize, bufferSize - currentBufferSize); + boolean atEnd = bytesRead == -1; + boolean fullBuffer = !atEnd && (bytesRead + currentBufferSize) == bufferSize; + if (!atEnd) currentBufferSize += bytesRead; + + if (atEnd || fullBuffer) { + totalBytes += currentBufferSize; + long currentLoc = offset + bufferSize * partNumber; + if (!atEnd) { + //verify not on end - try read another byte + bytesRead = input.read(nibbleBuffer, 0, 1); + atEnd = bytesRead == -1; + } + if (atEnd) { + if (length == -1) length = totalBytes; + byte[] finalBuffer = new byte[currentBufferSize]; + System.arraycopy(buffer, 0, finalBuffer, 0, currentBufferSize); + buffer = finalBuffer; + } + String range = String.format(Locale.US, "bytes %d-%d/%d", currentLoc, currentLoc + currentBufferSize - 1, length); + extraHeaders.put("Content-Range", range); + Map sentParams = new HashMap(); + sentParams.putAll(params); + + // wrap the callback with another callback to account for multiple parts + final long bytesUploadedSoFar = totalBytesUploaded; + final ProgressCallback singlePartProgressCallback; + if (progressCallback == null) { + singlePartProgressCallback = null; + } else { + singlePartProgressCallback = new ProgressCallback() { + + @Override + public void onProgress(long bytesUploaded, long totalBytes) { + progressCallback.onProgress(bytesUploadedSoFar + bytesUploaded, knownLengthBeforeUpload); + } + }; + } + + response = callApi("upload", sentParams, sentOptions, buffer, singlePartProgressCallback); + + if (atEnd) break; + buffer[0] = nibbleBuffer[0]; + totalBytesUploaded += currentBufferSize; + currentBufferSize = 1; + partNumber++; + } + } + return response; + } + + public Map destroy(String publicId, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + params.put("type", (String) options.get("type")); + params.put("public_id", publicId); + params.put("invalidate", ObjectUtils.asBoolean(options.get("invalidate"), false).toString()); + params.put("notification_url", (String) options.get("notification_url")); + return callApi("destroy", params, options, null); + } + + public Map rename(String fromPublicId, String toPublicId, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + params.put("type", (String) options.get("type")); + params.put("overwrite", ObjectUtils.asBoolean(options.get("overwrite"), false).toString()); + params.put("from_public_id", fromPublicId); + params.put("to_public_id", toPublicId); + params.put("invalidate", ObjectUtils.asBoolean(options.get("invalidate"), false).toString()); + params.put("to_type", options.get("to_type")); + params.put("context", ObjectUtils.asBoolean(options.get("context"), false).toString()); + params.put("metadata", ObjectUtils.asBoolean(options.get("metadata"), false).toString()); + params.put("notification_url", (String) options.get("notification_url")); + return callApi("rename", params, options, null); + } + + public Map explicit(String publicId, Map options) throws IOException { + if (options == null) { + options = ObjectUtils.emptyMap(); + } + Map params = buildUploadParams(options); + params.put("public_id", publicId); + return callApi("explicit", params, options, null); + } + + public Map generateSprite(String tag, Map options) throws IOException { + if (options == null) + options = Collections.singletonMap("tag", tag); + else + options.put("tag", tag); + + return callApi("sprite", buildGenerateSpriteParams(options), options, null); + } + + public Map generateSprite(String[] urls, Map options) throws IOException { + if (options == null) + options = Collections.singletonMap("urls", urls); + else + options.put("urls", urls); + + return callApi("sprite", buildGenerateSpriteParams(options), options, null); + } + + public Map multi(String[] urls, Map options) throws IOException { + if (options == null) { + options = Collections.singletonMap("urls", urls); + } else { + options.put("urls", urls); + } + + return multi(options); + } + + public Map multi(String tag, Map options) throws IOException { + if (options == null) { + options = Collections.singletonMap("tag", tag); + } else { + options.put("tag", tag); + } + + return multi(options); + } + + private Map multi(Map options) throws IOException { + return callApi("multi", buildMultiParams(options), options, null); + } + + public Map explode(String public_id, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + Object transformation = options.get("transformation"); + if (transformation != null) { + if (transformation instanceof Transformation) { + transformation = ((Transformation) transformation).generate(); + } + params.put("transformation", transformation.toString()); + } + params.put("public_id", public_id); + params.put("notification_url", (String) options.get("notification_url")); + params.put("format", (String) options.get("format")); + return callApi("explode", params, options, null); + } + + /** + * Add a tag to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param tag - The tag to assign. + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available parameters for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return A map with the public ids returned from the server + * @throws IOException + */ + public Map addTag(String tag, String[] publicIds, Map options) throws IOException { + return addTag(new String[]{tag}, publicIds, options); + } + + /** + * Add a tag to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param tag - An array of tags to assign. + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available parameters for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return A map with the public ids returned from the server. + * @throws IOException + */ + public Map addTag(String[] tag, String[] publicIds, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + boolean exclusive = ObjectUtils.asBoolean(options.get("exclusive"), false); + String command = exclusive ? "set_exclusive" : Command.add; + return callTagsApi(tag, command, publicIds, options); + } + + /** + * Remove a tag to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param tag - The tag to remove. + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available parameters for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return - A map with the public ids returned from the server. + * @throws IOException + */ + public Map removeTag(String tag, String[] publicIds, Map options) throws IOException { + return removeTag(new String[]{tag}, publicIds, options); + } + + /** + * Remove tags to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param tag - The array of tags to remove. + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available parameters for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return - * @return - A map with the public ids returned from the server. + * @throws IOException + */ + public Map removeTag(String[] tag, String[] publicIds, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + return callTagsApi(tag, Command.remove, publicIds, options); + } + + /** + * Remove an array of tags to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available parameters for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return - * @return - A map with the public ids returned from the server. + * @throws IOException + */ + public Map removeAllTags(String[] publicIds, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + return callTagsApi(null, Command.removeAll, publicIds, options); + } + + /** + * Replaces a tag to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param tag - The tag to replace. + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available options for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return - A map with the public ids returned from the server. + * @throws IOException + */ + public Map replaceTag(String tag, String[] publicIds, Map options) throws IOException { + return replaceTag(new String[]{tag}, publicIds, options); + } + + /** + * Replaces tags to one or more assets in your cloud. + * Tags are used to categorize and organize your images, and can also be used to apply group actions to images, + * for example to delete images, create sprites, ZIP files, JSON lists, or animated GIFs. + * Each image can be assigned one or more tags, which is a short name that you can dynamically use (no need to predefine tags). + * @param tag - An array of tag to replace. + * @param publicIds - An array of Public IDs of images uploaded to Cloudinary. + * @param options - An object holding the available options for the request. + * options may include 'exclusive' (boolean) which causes clearing this tag from all other resources + * @return - A map with the public ids returned from the server. + * @throws IOException + */ + public Map replaceTag(String[] tag, String[] publicIds, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + return callTagsApi(tag, Command.replace, publicIds, options); + } + + public Map callTagsApi(String[] tag, String command, String[] publicIds, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + if (tag != null) { + params.put("tag", StringUtils.join(tag, ",")); + } + params.put("command", command); + params.put("type", (String) options.get("type")); + params.put("public_ids", Arrays.asList(publicIds)); + return callApi("tags", params, options, null); + } + + /** + * Add a context keys and values. If a particular key already exists, the value associated with the key is updated. + * @param context a map of key and value. Serialized to "key1=value1|key2=value2" + * @param publicIds the public IDs of the resources to update + * @param options additional options passed to the request + * @return a list of public IDs that were updated + * @throws IOException + */ + public Map addContext(Map context, String[] publicIds, Map options) throws IOException { + return callContextApi(context, Command.add, publicIds, options); + } + + /** + * Add a context keys and values. If a particular key already exists, the value associated with the key is updated. + * @param context Serialized context in the form of "key1=value1|key2=value2" + * @param publicIds the public IDs of the resources to update + * @param options additional options passed to the request + * @return a list of public IDs that were updated + * @throws IOException + */ + public Map addContext(String context, String[] publicIds, Map options) throws IOException { + return callContextApi(context, Command.add, publicIds, options); + } + + /** + * Remove all custom context from the specified public IDs. + * @param publicIds the public IDs of the resources to update + * @param options additional options passed to the request + * @return a list of public IDs that were updated + * @throws IOException + */ + public Map removeAllContext(String[] publicIds, Map options) throws IOException { + return callContextApi((String)null, Command.removeAll, publicIds, options); + } + + protected Map callContextApi(Map context, String command, String[] publicIds, Map options) throws IOException { + return callContextApi(Util.encodeContext(context), command, publicIds, options); + } + + protected Map callContextApi(String context, String command, String[] publicIds, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + if (context != null) { + params.put("context", context); + } + params.put("command", command); + params.put("type", (String) options.get("type")); + params.put("public_ids", Arrays.asList(publicIds)); + return callApi("context", params, options, null); + } + + private final static String[] TEXT_PARAMS = {"public_id", "font_family", "font_size", "font_color", "text_align", "font_weight", "font_style", + "background", "opacity", "text_decoration"}; + public Map text(String text, Map options) throws IOException { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + params.put("text", text); + for (String param : TEXT_PARAMS) { + params.put(param, ObjectUtils.asString(options.get(param))); + } + return callApi("text", params, options, null); + } + + public Map createArchive(Map options, String targetFormat) throws IOException { + Map params = Util.buildArchiveParams(options, targetFormat); + return callApi("generate_archive", params, options, null); + } + + public Map createZip(Map options) throws IOException { + return createArchive(options, "zip"); + } + + public Map createArchive(ArchiveParams params) throws IOException { + return createArchive(params.toMap(), params.targetFormat()); + } + + public void signRequestParams(Map params, Map options) { + if (!params.containsKey("timestamp")) + params.put("timestamp", Util.timestamp()); + cloudinary.signRequest(params, options); + } + + public String uploadTagParams(Map options) { + if (options == null) + options = new HashMap(); + if (options.get("resource_type") == null) { + options = new HashMap(options); + options.put("resource_type", "auto"); + } + + String callback = ObjectUtils.asString(options.get("callback"), this.cloudinary.config.callback); + if (callback == null) { + throw new IllegalArgumentException("Must supply callback"); + } + options.put("callback", callback); + + Map params = this.buildUploadParams(options); + if (options.get("unsigned") == null || Boolean.FALSE.equals(options.get("unsigned"))) { + signRequestParams(params, options); + } else { + Util.clearEmpty(params); + } + + return JSONObject.valueToString(params); + } + + public String getUploadUrl(Map options) { + if (options == null) + options = new HashMap(); + return this.cloudinary.cloudinaryApiUrl("upload", options); + } + + public String unsignedImageUploadTag(String field, String uploadPreset, Map options, Map htmlOptions) { + Map nextOptions = new HashMap(options); + nextOptions.put("upload_preset", uploadPreset); + nextOptions.put("unsigned", true); + return imageUploadTag(field, nextOptions, htmlOptions); + } + + public String imageUploadTag(String field, Map options, Map htmlOptions) { + if (htmlOptions == null) + htmlOptions = ObjectUtils.emptyMap(); + + String tagParams = StringUtils.escapeHtml(uploadTagParams(options)); + + String cloudinaryUploadUrl = getUploadUrl(options); + + StringBuilder builder = new StringBuilder(); + builder.append(""); + return builder.toString(); + } + + public Map deleteByToken(String token) throws Exception { + return callApi("delete_by_token", ObjectUtils.asMap("token", token), ObjectUtils.emptyMap(), null); + } + + /** + * Populates metadata fields with the given values. Existing values will be overwritten. + * @param metadata a map of field name and value. + * @param publicIds the public IDs of the resources to update + * @param options additional options passed to the request + * @return a list of public IDs that were updated + * @throws IOException + */ + public Map updateMetadata(Map metadata, String[] publicIds, Map options) throws IOException { + if (options == null) + options = new HashMap(); + + Map params = new HashMap(); + params.put("metadata", Util.encodeContext(metadata)); + params.put("public_ids", Arrays.asList(publicIds)); + params.put("type", (String)options.get("type")); + + return callApi("metadata", params, options, null); + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Url.java b/cloudinary-core/src/main/java/com/cloudinary/Url.java index c6bc6bb6..5365c996 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Url.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Url.java @@ -1,276 +1,748 @@ package com.cloudinary; import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URL; import java.net.URLDecoder; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.zip.CRC32; -import org.apache.commons.codec.binary.Base64; -import org.apache.commons.codec.binary.Hex; -import org.apache.commons.lang.StringUtils; +import com.cloudinary.utils.Base64Coder; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import static com.cloudinary.SignatureAlgorithm.SHA256; public class Url { - String cloudName; - boolean secure; - boolean privateCdn; - String secureDistribution; - boolean cdnSubdomain; - boolean shorten; - boolean signUrl; - String cname; - String type = "upload"; - String resourceType = "image"; - String format = null; - String version = null; + private final Cloudinary cloudinary; + private final Configuration config; + private boolean longUrlSignature; + String publicId = null; + String type = null; + String resourceType = null; + String format = null; + String version = null; + Transformation transformation = null; + boolean signUrl; + private AuthToken authToken; String source = null; - String apiSecret = null; - Transformation transformation = null; - private static final String CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; - - public Url(Cloudinary cloudinary) { - this.cloudName = cloudinary.getStringConfig("cloud_name"); - this.secureDistribution = cloudinary.getStringConfig("secure_distribution"); - this.cname = cloudinary.getStringConfig("cname"); - this.secure = cloudinary.getBooleanConfig("secure", false); - this.privateCdn = cloudinary.getBooleanConfig("private_cdn", false); - this.cdnSubdomain = cloudinary.getBooleanConfig("cdn_subdomain", false); - this.shorten = cloudinary.getBooleanConfig("shorten", false); - this.signUrl = cloudinary.getBooleanConfig("sign_url", false); - this.apiSecret = cloudinary.getStringConfig("api_secret"); - } - - public Url type(String type) { - this.type = type; - return this; - } - - @Deprecated - public Url resourcType(String resourceType) { - return resourceType(resourceType); - } - - public Url resourceType(String resourceType) { - this.resourceType = resourceType; - return this; - } - - public Url format(String format) { - this.format = format; - return this; - } - - public Url cloudName(String cloudName) { - this.cloudName = cloudName; - return this; - } - - public Url secureDistribution(String secureDistribution) { - this.secureDistribution = secureDistribution; - return this; - } - - public Url cname(String cname) { - this.cname = cname; - return this; - } - - public Url version(Object version) { - this.version = Cloudinary.asString(version); - return this; - } - - public Url transformation(Transformation transformation) { - this.transformation = transformation; - return this; - } - - public Url secure(boolean secure) { - this.secure = secure; - return this; - } - - public Url privateCdn(boolean privateCdn) { - this.privateCdn = privateCdn; - return this; - } - - public Url cdnSubdomain(boolean cdnSubdomain) { - this.cdnSubdomain = cdnSubdomain; - return this; - } - - public Url shorten(boolean shorten) { - this.shorten = shorten; - return this; - } + private String urlSuffix; + private Boolean useRootPath; + private Boolean useFetchFormat; + Map sourceTransformation = null; + String[] sourceTypes = null; + String fallbackContent = null; + Transformation posterTransformation = null; + String posterSource = null; + Url posterUrl = null; - public Url source(String source) { - this.source = source; - return this; + private static final String CL_BLANK = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; + public static final String[] DEFAULT_VIDEO_SOURCE_TYPES = {"webm", "mp4", "ogv"}; + private static final Pattern VIDEO_EXTENSION_RE = Pattern.compile("\\.(" + StringUtils.join(DEFAULT_VIDEO_SOURCE_TYPES, "|") + ")$"); + + public Url(Cloudinary cloudinary) { + this.cloudinary = cloudinary; + this.config = new Configuration(cloudinary.config); + this.longUrlSignature = config.longUrlSignature; + this.authToken = config.authToken; } - public Url source(StoredFile source) { - if (source.getResourceType() != null) this.resourceType = source.getResourceType(); - if (source.getType() != null) this.type = source.getType(); - if (source.getVersion() != null) this.version = source.getVersion().toString(); - this.format = source.getFormat(); - this.source = source.getPublicId(); + public Url clone() { + Url cloned = cloudinary.url(); + cloned.config.update(config.asMap()); + cloned.fallbackContent = this.fallbackContent; + cloned.format = this.format; + cloned.posterSource = this.posterSource; + if (this.posterTransformation != null) + cloned.posterTransformation = new Transformation(this.posterTransformation); + if (this.posterUrl != null) cloned.posterUrl = this.posterUrl.clone(); + cloned.publicId = this.publicId; + cloned.resourceType = this.resourceType; + cloned.signUrl = this.signUrl; + cloned.source = this.source; + if (this.transformation != null) cloned.transformation = new Transformation(this.transformation); + if (this.sourceTransformation != null) { + cloned.sourceTransformation = new HashMap(); + for (Map.Entry keyValuePair : this.sourceTransformation.entrySet()) { + cloned.sourceTransformation.put(keyValuePair.getKey(), keyValuePair.getValue()); + } + } + cloned.sourceTypes = this.sourceTypes; + cloned.urlSuffix = this.urlSuffix; + cloned.useRootPath = this.useRootPath; + cloned.useFetchFormat = this.useFetchFormat; + cloned.longUrlSignature = this.longUrlSignature; + cloned.authToken = this.authToken; + return cloned; + } + + private static Pattern identifierPattern = Pattern.compile("^(?:([^/]+)/)??(?:([^/]+)/)??(?:v(\\d+)/)?" + "(?:([^#/]+?)(?:\\.([^.#/]+))?)(?:#([^/]+))?$"); + + /** + * Parses a cloudinary identifier of the form:
+ * {@code [/][/][v/][.][#]} + */ + public Url fromIdentifier(String identifier) { + Matcher matcher = identifierPattern.matcher(identifier); + if (!matcher.matches()) { + throw new RuntimeException(String.format("Couldn't parse identifier %s", identifier)); + } + + String resourceType = matcher.group(1); + if (resourceType != null) { + resourceType(resourceType); + } + + String type = matcher.group(2); + if (type != null) { + type(type); + } + + String version = matcher.group(3); + if (version != null) { + version(version); + } + + String publicId = matcher.group(4); + if (publicId != null) { + publicId(publicId); + } + + String format = matcher.group(5); + if (format != null) { + format(format); + } + + // Signature (group 6) is not used + + return this; + } + + public Url type(String type) { + this.type = type; + return this; + } + + public Url resourcType(String resourceType) { + return resourceType(resourceType); + } + + public Url resourceType(String resourceType) { + this.resourceType = resourceType; + return this; + } + + public Url publicId(Object publicId) { + this.publicId = ObjectUtils.asString(publicId); + return this; + } + + public Url format(String format) { + this.format = format; + return this; + } + + public Url cloudName(String cloudName) { + this.config.cloudName = cloudName; + return this; + } + + public Url secureDistribution(String secureDistribution) { + this.config.secureDistribution = secureDistribution; + return this; + } + + public Url secureCdnSubdomain(boolean secureCdnSubdomain) { + this.config.secureCdnSubdomain = secureCdnSubdomain; + return this; + } + + public Url suffix(String urlSuffix) { + this.urlSuffix = urlSuffix; + return this; + } + + public Url useRootPath(boolean useRootPath) { + this.useRootPath = useRootPath; + return this; + } + + public Url useFetchFormat(boolean useFetchFormat) { + this.useFetchFormat = useFetchFormat; + return this; + } + + public Url cname(String cname) { + this.config.cname = cname; + return this; + } + + public Url version(Object version) { + this.version = ObjectUtils.asString(version); return this; } - public Transformation transformation() { - if (this.transformation == null) - this.transformation = new Transformation(); - return this.transformation; - } - - public Url signed(boolean signUrl) { - this.signUrl = signUrl; - return this; - } + public Url transformation(Transformation transformation) { + this.transformation = transformation; + return this; + } + + public Url secure(boolean secure) { + this.config.secure = secure; + return this; + } + + public Url privateCdn(boolean privateCdn) { + this.config.privateCdn = privateCdn; + return this; + } + + public Url cdnSubdomain(boolean cdnSubdomain) { + this.config.cdnSubdomain = cdnSubdomain; + return this; + } + + public Url shorten(boolean shorten) { + this.config.shorten = shorten; + return this; + } + + public Transformation transformation() { + if (this.transformation == null) + this.transformation = new Transformation(); + return this.transformation; + } + + public Url signed(boolean signUrl) { + this.signUrl = signUrl; + return this; + } + + /** + * Set the authorization token. If authToken has already been set the parameter is merged with the current value unless the parameter value is null or NULL_AUTH_TOKEN.

+ * For example, to generate an authorized URL with a different duration:
+ *

+     *  {@code
+     *   cloudinary.config.authToken = new AuthToken(KEY).duration(500);
+     *   // later...
+     *   cloudinary.url().signed(true).authToken(new AuthToken().duration(300))
+     *                   .type("authenticated").version("1486020273").generate("sample.jpg");
+     *  }
+     * 
+ * + * @param authToken an authorization token object + * @return this + */ + public Url authToken(AuthToken authToken) { + if (this.authToken == null) { + this.authToken = authToken; + } else if (authToken == null || authToken.equals(AuthToken.NULL_AUTH_TOKEN)) { + this.authToken = authToken; + } else { + this.authToken = this.authToken.merge(authToken); + } + return this; + } + + public Url longUrlSignature(boolean isLong) { + this.longUrlSignature = isLong; + return this; + } + + public Url sourceTransformation(Map sourceTransformation) { + this.sourceTransformation = sourceTransformation; + return this; + } + + public Url sourceTransformationFor(String source, Transformation transformation) { + if (this.sourceTransformation == null) { + this.sourceTransformation = new HashMap(); + } + this.sourceTransformation.put(source, transformation); + return this; + } + + public Url sourceTypes(String[] sourceTypes) { + this.sourceTypes = sourceTypes; + return this; + } + + public Url fallbackContent(String fallbackContent) { + this.fallbackContent = fallbackContent; + return this; + } + + public Url posterTransformation(Transformation posterTransformation) { + this.posterTransformation = posterTransformation; + return this; + } + + @SuppressWarnings("rawtypes") + public Url posterTransformation(List posterTransformations) { + this.posterTransformation = new Transformation(posterTransformations); + return this; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public Url posterTransformation(Map posterTransformations) { + List transformations = new ArrayList(); + Map copy = new HashMap(); + copy.putAll(posterTransformations); + transformations.add(copy); + this.posterTransformation = new Transformation(transformations); + return this; + } + + public Url posterSource(String posterSource) { + this.posterSource = posterSource; + return this; + } + + public Url posterUrl(Url posterUrl) { + this.posterUrl = posterUrl; + return this; + } + + public Url poster(Object poster) { + if (poster instanceof Transformation) { + return posterTransformation((Transformation) poster); + } else if (poster instanceof List) { + return posterTransformation((List) poster); + } else if (poster instanceof Map) { + return posterTransformation((Map) poster); + } else if (poster instanceof Url) { + return posterUrl((Url) poster); + } else if (poster instanceof String) { + return posterSource((String) poster); + } else if (poster == null || poster.equals(Boolean.FALSE)) { + return posterSource(""); + } else { + throw new IllegalArgumentException("Illegal value type supplied to poster. must be one of: , >, , , "); + } + } + + /** + * Indicates whether to add '/v1/' to the URL when the public ID includes folders and a 'version' value was + * not defined. + * When no version is explicitly specified and the public id contains folders, a default v1 version + * is added to the url. This boolean can disable that behaviour. + * @param forceVersion Whether to add the version to the url. + * @return This same Url instance for chaining. + */ + public Url forceVersion(boolean forceVersion){ + this.config.forceVersion = forceVersion; + return this; + } + + public String generate() { + return generate(null); + } public String generate(String source) { - this.source = source; - return this.generate(); - } - - public String generate() { - if (type.equals("fetch") && StringUtils.isNotBlank(format)) { - transformation().fetchFormat(format); - this.format = null; - } - String transformationStr = transformation().generate(); - if (StringUtils.isBlank(this.cloudName)) { - throw new IllegalArgumentException("Must supply cloud_name in tag or in configuration"); - } - - if (source == null) - return null; - String original_source = source; - - if (source.toLowerCase().matches("^https?:/.*")) { - if ("upload".equals(type) || "asset".equals(type)) { - return original_source; - } - source = SmartUrlEncoder.encode(source); - } else { - try { - source = SmartUrlEncoder.encode(URLDecoder.decode(source.replace("+", "%2B"), "UTF-8")); - } catch (UnsupportedEncodingException e) { - throw new RuntimeException(e); - } - if (format != null) source = source + "." + format; - } - String prefix; - boolean sharedDomain = !privateCdn; - if (secure) { - if (StringUtils.isBlank(secureDistribution) || Cloudinary.OLD_AKAMAI_SHARED_CDN.equals(secureDistribution)) { - secureDistribution = privateCdn ? cloudName + "-res.cloudinary.com" : Cloudinary.SHARED_CDN; + boolean useRootPath = this.config.useRootPath; + if (this.useRootPath != null) { + useRootPath = this.useRootPath; + } + + if (StringUtils.isEmpty(this.config.cloudName)) { + throw new IllegalArgumentException("Must supply cloud_name in tag or in configuration"); + } + + if (source == null) { + if (publicId == null) { + if (this.source == null) { + return null; + } + source = this.source; + } else { + source = publicId; + } + } + + boolean httpSource = StringUtils.isHttpUrl(source); + if (httpSource) { + if (StringUtils.isEmpty(type) || "asset".equals(type)) { + return source; + } + } + + if ((type != null && type.equals("fetch") || (useFetchFormat != null && useFetchFormat)) && !StringUtils.isEmpty(format)) { + transformation().fetchFormat(format); + this.format = null; + } + + String transformationStr = transformation().generate(); + String signature = ""; + + String[] finalizedSource = finalizeSource(source, format, urlSuffix); + source = finalizedSource[0]; + String sourceToSign = finalizedSource[1]; + + if (this.config.forceVersion && sourceToSign.contains("/") && !StringUtils.startWithVersionString(sourceToSign) && + !httpSource && StringUtils.isEmpty(version)) { + version = "1"; + } + + if (version == null) + version = ""; + else + version = "v" + version; + + + if (signUrl && (authToken == null || authToken.equals(AuthToken.NULL_AUTH_TOKEN))) { + SignatureAlgorithm signatureAlgorithm = longUrlSignature ? SHA256 : config.signatureAlgorithm; + + String toSign = StringUtils.join(new String[]{transformationStr, sourceToSign}, "/"); + toSign = StringUtils.removeStartingChars(toSign, '/'); + toSign = StringUtils.mergeSlashesInUrl(toSign); + + byte[] hash = Util.hash(toSign + this.config.apiSecret, signatureAlgorithm); + signature = Base64Coder.encodeURLSafeString(hash); + signature = "s--" + signature.substring(0, longUrlSignature ? 32 : 8) + "--"; + } + + String resourceType = this.resourceType; + if (resourceType == null) resourceType = "image"; + String finalResourceType = finalizeResourceType(resourceType, type, urlSuffix, useRootPath, config.shorten); + String prefix = unsignedDownloadUrlPrefix(source, config); + String join = StringUtils.join(new String[]{prefix, finalResourceType, signature, transformationStr, version, source}, "/"); + String url = StringUtils.mergeSlashesInUrl(join); + + if (signUrl && authToken != null && !authToken.equals(AuthToken.NULL_AUTH_TOKEN)) { + try { + URL tempUrl = new URL(url); + String path = tempUrl.getPath(); + String token = authToken.generate(path); + url = url + "?" + token; + } catch (MalformedURLException ignored) { + } + } + if (cloudinary.config.analytics != null && cloudinary.config.analytics) { + try { + URL tempUrl = new URL(url); + // if any other query param already exist on the URL do not add analytics query param. + if (tempUrl.getQuery() == null) { + String path = tempUrl.getPath(); + url = url + "?" + cloudinary.analytics.toQueryParam(); + } + } catch (MalformedURLException ignored) { + } + } + return url; + } + + private String[] finalizeSource(String source, String format, String urlSuffix) { + source = StringUtils.mergeSlashesInUrl(source); + String[] result = new String[2]; + String sourceToSign; + if (StringUtils.isHttpUrl(source)) { + source = SmartUrlEncoder.encode(source); + sourceToSign = source; + } else { + try { + source = SmartUrlEncoder.encode(URLDecoder.decode(source.replace("+", "%2B"), "UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + sourceToSign = source; + if (StringUtils.isNotBlank(urlSuffix)) { + if (urlSuffix.contains(".") || urlSuffix.contains("/")) { + throw new IllegalArgumentException("url_suffix should not include . or /"); + } + source = source + "/" + urlSuffix; + } + if (StringUtils.isNotBlank(format)) { + source = source + "." + format; + sourceToSign = sourceToSign + "." + format; + } + } + result[0] = source; + result[1] = sourceToSign; + return result; + } + + public String finalizeResourceType(String resourceType, String type, String urlSuffix, boolean useRootPath, boolean shorten) { + if (type == null) { + type = "upload"; + } + + if (!StringUtils.isBlank(urlSuffix)) { + if (resourceType.equals("image") && type.equals("upload")) { + resourceType = "images"; + type = null; + } else if (resourceType.equals("image") && type.equals("private")) { + resourceType = "private_images"; + type = null; + } else if (resourceType.equals("image") && type.equals("authenticated")) { + resourceType = "authenticated_images"; + type = null; + } else if (resourceType.equals("raw") && type.equals("upload")) { + resourceType = "files"; + type = null; + } else if (resourceType.equals("video") && type.equals("upload")) { + resourceType = "videos"; + type = null; + } else { + throw new IllegalArgumentException("URL Suffix only supported for image/upload, image/private, raw/upload, image/authenticated and video/upload"); + } + } + if (useRootPath) { + if ((resourceType.equals("image") && type.equals("upload")) || (resourceType.equals("images") && StringUtils.isBlank(type))) { + resourceType = null; + type = null; + } else { + throw new IllegalArgumentException("Root path only supported for image/upload"); } - sharedDomain = sharedDomain || Cloudinary.SHARED_CDN.equals(secureDistribution); + } + if (shorten && resourceType.equals("image") && type.equals("upload")) { + resourceType = "iu"; + type = null; + } + String result = resourceType; + if (type != null) { + result += "/" + type; + } + return result; + } + + public static String unsignedDownloadUrlPrefix(String source, Configuration config) { + if (config.cloudName.startsWith("/")) { + return "/res" + config.cloudName; + } + boolean sharedDomain = !config.privateCdn; + + String prefix; + String cloudName; + String secureDistribution = config.secureDistribution; + Boolean secureCdnSubdomain = null; + + if (config.secure) { + if (StringUtils.isEmpty(config.secureDistribution) || config.secureDistribution.equals(Cloudinary.OLD_AKAMAI_SHARED_CDN)) { + secureDistribution = config.privateCdn ? config.cloudName + "-res.cloudinary.com" : Cloudinary.SHARED_CDN; + } + if (!sharedDomain) { + sharedDomain = secureDistribution.equals(Cloudinary.SHARED_CDN); + } + + if (secureCdnSubdomain == null && sharedDomain) { + secureCdnSubdomain = config.cdnSubdomain; + } + + if (secureCdnSubdomain != null && secureCdnSubdomain == true) { + secureDistribution = config.secureDistribution.replace("res.cloudinary.com", "res-" + shard(source) + ".cloudinary.com"); + } + prefix = "https://" + secureDistribution; + } else if (StringUtils.isNotBlank(config.cname)) { + String subdomain = config.cdnSubdomain ? "a" + shard(source) + "." : ""; + prefix = "http://" + subdomain + config.cname; } else { - CRC32 crc32 = new CRC32(); - crc32.update(source.getBytes()); - String subdomain = cdnSubdomain ? "a" + ((crc32.getValue() % 5 + 5) % 5 + 1) + "." : ""; - String host = cname != null ? cname : (privateCdn ? cloudName + "-" : "") + "res.cloudinary.com"; - prefix = "http://" + subdomain + host; - } - if (sharedDomain) prefix = prefix + "/" + cloudName; - - if (shorten && resourceType.equals("image") && type.equals("upload")) { - resourceType = "iu"; - type = ""; - } - - if (source.contains("/") && !source.matches("v[0-9]+.*") && !source.matches("https?:/.*") && StringUtils.isBlank(version)) { - version = "1"; - } - - if (version != null) - version = "v" + version; - - String rest = StringUtils.join(new String[] {transformationStr, version, source }, "/"); - rest = rest.replaceAll("^/+", "").replaceAll("([^:])\\/+", "$1/"); - - if (signUrl) { - MessageDigest md = null; - try { - md = MessageDigest.getInstance("SHA-1"); - } - catch(NoSuchAlgorithmException e) { - throw new RuntimeException("Unexpected exception", e); - } - byte[] digest = md.digest((rest + apiSecret).getBytes()); - String signature = Base64.encodeBase64URLSafeString(digest); - rest = "s--" + signature.substring(0, 8) + "--/" + rest; - } - - return StringUtils.join(new String[] { prefix, resourceType, type, rest }, "/").replaceAll("([^:])\\/+", "$1/"); - } - - public String generateSpriteCss(String source) { - this.type = "sprite"; - if (!source.endsWith(".css")) this.format = "css"; - return generate(source); - } - - public String imageTag(String source) { - return imageTag(source, Cloudinary.emptyMap()); - } + String protocol = "http://"; + cloudName = config.privateCdn ? config.cloudName + "-" : ""; + String res = "res"; + String subdomain = config.cdnSubdomain ? "-" + shard(source) : ""; + String domain = ".cloudinary.com"; + prefix = StringUtils.join(new String[]{protocol, cloudName, res, subdomain, domain}, ""); + } + if (sharedDomain) { + prefix += "/" + config.cloudName; + } + return prefix; + } + + private static String shard(String input) { + CRC32 crc32 = new CRC32(); + crc32.update(Util.getUTF8Bytes(input)); + return String.valueOf((crc32.getValue() % 5 + 5) % 5 + 1); + } + + @SuppressWarnings("unchecked") + public String imageTag(String source) { + return imageTag(source, ObjectUtils.emptyMap()); + } + + public String imageTag(Map attributes) { + return imageTag(null, attributes); + } public String imageTag(String source, Map attributes) { + String url = generate(source); + attributes = new TreeMap(attributes); // Make sure they + // are ordered. + if (transformation().getHtmlHeight() != null) + attributes.put("height", transformation().getHtmlHeight()); + if (transformation().getHtmlWidth() != null) + attributes.put("width", transformation().getHtmlWidth()); + + boolean hiDPI = transformation().isHiDPI(); + boolean responsive = transformation().isResponsive(); + + if (!config.clientHints && (hiDPI || responsive)) { + attributes.put("data-src", url); + String extraClass = responsive ? "cld-responsive" : "cld-hidpi"; + attributes.put("class", (StringUtils.isBlank(attributes.get("class")) ? "" : attributes.get("class") + " ") + extraClass); + String responsivePlaceholder = attributes.remove("responsive_placeholder"); + if ("blank".equals(responsivePlaceholder)) { + responsivePlaceholder = CL_BLANK; + } + url = responsivePlaceholder; + } + + StringBuilder builder = new StringBuilder(); + builder.append(" attr : attributes.entrySet()) { + builder.append(" ").append(attr.getKey()).append("='").append(attr.getValue()).append("'"); + } + builder.append("/>"); + return builder.toString(); + } + + public String videoTag() { + return videoTag("", new HashMap()); + } + + public String videoTag(String source) { + return videoTag(source, new HashMap()); + } + + public String videoTag(Map attributes) { + return videoTag("", attributes); + } + + private String finalizePosterUrl(String source) { + String posterUrl = null; + if (this.posterUrl != null) { + posterUrl = this.posterUrl.generate(); + } else if (this.posterTransformation != null) { + posterUrl = this.clone().format("jpg").transformation(new Transformation(this.posterTransformation)) + .generate(source); + } else if (this.posterSource != null) { + if (!StringUtils.isEmpty(this.posterSource)) + posterUrl = this.clone().format("jpg").generate(this.posterSource); + } else { + posterUrl = this.clone().format("jpg").generate(source); + } + return posterUrl; + } + + private void appendVideoSources(StringBuilder html, String source, String sourceType) { + Url sourceUrl = this.clone(); + if (this.sourceTransformation != null) { + Transformation transformation = this.transformation; + Transformation sourceTransformation = null; + if (this.sourceTransformation.get(sourceType) != null) + sourceTransformation = new Transformation(this.sourceTransformation.get(sourceType)); + if (transformation == null) { + transformation = sourceTransformation; + } else if (sourceTransformation != null) { + transformation = transformation.chainWith(sourceTransformation); + } + sourceUrl.transformation(transformation); + } + String src = sourceUrl.format(sourceType).generate(source); + String videoType = sourceType; + if (sourceType.equals("ogv")) + videoType = "ogg"; + String mimeType = "video/" + videoType; + html.append(""); + } + + public String videoTag(String source, Map attributes) { + if (StringUtils.isEmpty(source)) + source = this.source; + if (StringUtils.isEmpty(source)) + source = publicId; + if (StringUtils.isEmpty(source)) + throw new IllegalArgumentException("must supply source or public id"); + source = VIDEO_EXTENSION_RE.matcher(source).replaceFirst(""); + + if (this.resourceType == null) this.resourceType = "video"; + attributes = new TreeMap(attributes); // Make sure they are ordered. + + String[] sourceTypes = this.sourceTypes; + + if (sourceTypes == null) { + sourceTypes = DEFAULT_VIDEO_SOURCE_TYPES; + } + + String posterUrl = this.finalizePosterUrl(source); + + if (!StringUtils.isEmpty(posterUrl)) + attributes.put("poster", posterUrl); + + StringBuilder html = new StringBuilder().append(" 1; + if (!multiSource) { + url = generate(source + "." + sourceTypes[0]); + attributes.put("src", url); + } else { + generate(source); + } + + if (this.transformation.getHtmlHeight() != null) + attributes.put("height", this.transformation.getHtmlHeight()); + if (attributes.containsKey("html_height")) + attributes.put("height", attributes.remove("html_height")); + if (this.transformation.getHtmlWidth() != null) + attributes.put("width", this.transformation.getHtmlWidth()); + if (attributes.containsKey("html_width")) + attributes.put("width", attributes.remove("html_width")); + + for (Map.Entry attr : attributes.entrySet()) { + html.append(" ").append(attr.getKey()); + if (attr.getValue() != null) { + String value = ObjectUtils.asString(attr.getValue()); + html.append("='").append(value).append("'"); + } + } + + html.append(">"); + + if (multiSource) { + for (String sourceType : sourceTypes) { + this.appendVideoSources(html, source, sourceType); + } + } + + if (this.fallbackContent != null) + html.append(this.fallbackContent); + html.append(""); + return html.toString(); + } + + public String generateSpriteCss(String source) { + this.type = "sprite"; + if (!source.endsWith(".css")) + this.format = "css"; + return generate(source); + } + + public Url source(String source) { this.source = source; - return imageTag(attributes); - } - - public String imageTag() { - return imageTag(Cloudinary.emptyMap()); - } - - public String imageTag(StoredFile source) { - return imageTag(source, Cloudinary.emptyMap()); - } - - public String imageTag(StoredFile source, Map attributes) { - source(source); - return imageTag(attributes); - } - - public String imageTag(Map attributes) { - String url = generate(); - attributes = new TreeMap(attributes); // Make sure they are ordered. - if (transformation().getHtmlHeight() != null) - attributes.put("height", transformation().getHtmlHeight()); - if (transformation().getHtmlWidth() != null) - attributes.put("width", transformation().getHtmlWidth()); - - boolean hiDPI = transformation().isHiDPI(); - boolean responsive = transformation().isResponsive(); - - if (hiDPI || responsive) { - attributes.put("data-src", url); - String extraClass = responsive ? "cld-responsive" : "cld-hidpi"; - attributes.put("class", (StringUtils.isBlank(attributes.get("class")) ? "" : attributes.get("class") + " ") + extraClass); - String responsivePlaceholder = attributes.remove("responsive_placeholder"); - if ("blank".equals(responsivePlaceholder)) { - responsivePlaceholder = CL_BLANK; - } - url = responsivePlaceholder; - } - - StringBuilder builder = new StringBuilder(); - builder.append(" attr : attributes.entrySet()) { - builder.append(" ").append(attr.getKey()).append("='").append(attr.getValue()).append("'"); - } - builder.append("/>"); - return builder.toString(); - } + return this; + } + + public Url source(StoredFile source) { + if (source.getResourceType() != null) + this.resourceType = source.getResourceType(); + if (source.getType() != null) + this.type = source.getType(); + if (source.getVersion() != null) + this.version = source.getVersion().toString(); + this.format = source.getFormat(); + this.source = source.getPublicId(); + return this; + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/Util.java b/cloudinary-core/src/main/java/com/cloudinary/Util.java index 1b5dd40e..4f15c220 100644 --- a/cloudinary-core/src/main/java/com/cloudinary/Util.java +++ b/cloudinary-core/src/main/java/com/cloudinary/Util.java @@ -1,129 +1,439 @@ package com.cloudinary; -import org.apache.commons.lang.StringUtils; - -import java.awt.Rectangle; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; - -public class Util { - static final String[] BOOLEAN_UPLOAD_OPTIONS = new String[] { - "backup", "exif", "faces", "colors", "image_metadata", "use_filename", "unique_filename", - "eager_async", "invalidate", "discard_original_filename", "overwrite", "phash", "return_delete_token"}; - - protected static final Map buildUploadParams(Map options) { - if (options == null) options = Cloudinary.emptyMap(); - Map params = new HashMap(); - Object transformation = options.get("transformation"); - if (transformation != null) { - if (transformation instanceof Transformation) { - transformation = ((Transformation) transformation).generate(); - } - params.put("transformation", transformation.toString()); - } - params.put("public_id", (String) options.get("public_id")); - params.put("callback", (String) options.get("callback")); - params.put("format", (String) options.get("format")); - params.put("type", (String) options.get("type")); - for (String attr : BOOLEAN_UPLOAD_OPTIONS) { - Boolean value = Cloudinary.asBoolean(options.get(attr), null); - if (value != null) - params.put(attr, value.toString()); - } - params.put("eager", buildEager((List) options.get("eager"))); - params.put("notification_url", (String) options.get("notification_url")); - params.put("eager_notification_url", (String) options.get("eager_notification_url")); - params.put("proxy", (String) options.get("proxy")); - params.put("folder", (String) options.get("folder")); - params.put("allowed_formats", StringUtils.join(Cloudinary.asArray(options.get("allowed_formats")), ",")); - params.put("moderation", options.get("moderation")); - params.put("upload_preset", options.get("upload_preset")); - - processWriteParameters(options, params); - return params; - } - - protected static final String buildEager(List transformations) { - if (transformations == null) { - return null; - } - List eager = new ArrayList(); - for (Transformation transformation : transformations) { - List single_eager = new ArrayList(); - String transformationString = transformation.generate(); - if (StringUtils.isNotBlank(transformationString)) { - single_eager.add(transformationString); - } - if (transformation instanceof EagerTransformation) { - EagerTransformation eagerTransformation = (EagerTransformation) transformation; - if (StringUtils.isNotBlank(eagerTransformation.getFormat())) { - single_eager.add(eagerTransformation.getFormat()); - } - } - eager.add(StringUtils.join(single_eager, "/")); - } - return StringUtils.join(eager, "|"); - } - - protected static final void processWriteParameters( - Map options, Map params) { - if (options.get("headers") != null) - params.put("headers", buildCustomHeaders(options.get("headers"))); - if (options.get("tags") != null) - params.put("tags", StringUtils.join( - Cloudinary.asArray(options.get("tags")), ",")); - if (options.get("face_coordinates") != null) - params.put("face_coordinates", Coordinates.parseCoordinates(options.get("face_coordinates")) - .toString()); - if (options.get("custom_coordinates") != null) - params.put("custom_coordinates", Coordinates.parseCoordinates(options.get("custom_coordinates")) - .toString()); - if (options.get("context") != null) - params.put("context", Cloudinary.encodeMap(options.get("context"))); - if (options.get("ocr") != null) - params.put("ocr", options.get("ocr")); - if (options.get("raw_convert") != null) - params.put("raw_convert", options.get("raw_convert")); - if (options.get("categorization") != null) - params.put("categorization", options.get("categorization")); - if (options.get("detection") != null) - params.put("detection", options.get("detection")); - if (options.get("similarity_search") != null) - params.put("similarity_search", options.get("similarity_search")); - if (options.get("background_removal") != null) - params.put("background_removal", options.get("background_removal")); - if (options.get("auto_tagging") != null) - params.put("auto_tagging", - Cloudinary.asFloat(options.get("auto_tagging"))); - } - - protected static final String buildCustomHeaders(Object headers) { - if (headers == null) { - return null; - } else if (headers instanceof String) { - return (String) headers; - } else if (headers instanceof Object[]) { - return StringUtils.join((Object[]) headers, "\n") + "\n"; - } else { - Map headersMap = (Map) headers; - StringBuilder builder = new StringBuilder(); - for (Map.Entry header : headersMap.entrySet()) { - builder.append(header.getKey()).append(": ") - .append(header.getValue()).append("\n"); - } - return builder.toString(); - } - } - - protected static void clearEmpty(Map params){ - for (Iterator iterator = params.values().iterator(); iterator.hasNext();) { - Object value = iterator.next(); - if (value == null || "".equals(value)) { - iterator.remove(); - } - } - } +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.cloudinary.json.JSONObject; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.*; + +public final class Util { + private Util() {} + + static final String[] BOOLEAN_UPLOAD_OPTIONS = new String[]{"backup", "exif", "faces", "colors", "image_metadata", "use_filename", "unique_filename", + "eager_async", "invalidate", "discard_original_filename", "overwrite", "phash", "return_delete_token", "async", "quality_analysis", "cinemagraph_analysis", + "accessibility_analysis", "use_filename_as_display_name", "use_asset_folder_as_public_id_prefix", "unique_display_name", "media_metadata", "visual_search", + "auto_chaptering", "auto_transcription"}; + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Map buildUploadParams(Map options) { + if (options == null) + options = ObjectUtils.emptyMap(); + Map params = new HashMap(); + + params.put("public_id", (String) options.get("public_id")); + params.put("callback", (String) options.get("callback")); + params.put("format", (String) options.get("format")); + params.put("type", (String) options.get("type")); + for (String attr : BOOLEAN_UPLOAD_OPTIONS) { + putBoolean(attr, options, params); + } + + params.put("eval",(String) options.get("eval")); + params.put("notification_url", (String) options.get("notification_url")); + params.put("eager_notification_url", (String) options.get("eager_notification_url")); + params.put("proxy", (String) options.get("proxy")); + params.put("folder", (String) options.get("folder")); + params.put("allowed_formats", StringUtils.join(ObjectUtils.asArray(options.get("allowed_formats")), ",")); + params.put("moderation", options.get("moderation")); + params.put("access_mode", (String) options.get("access_mode")); + params.put("filename_override", (String) options.get("filename_override")); + params.put("public_id_prefix", (String) options.get("public_id_prefix")); + params.put("asset_folder", (String) options.get("asset_folder")); + params.put("display_name", (String) options.get("display_name")); + params.put("on_success", (String) options.get("on_success")); + Object responsive_breakpoints = options.get("responsive_breakpoints"); + if (responsive_breakpoints != null) { + params.put("responsive_breakpoints", JSONObject.wrap(responsive_breakpoints)); + } + params.put("upload_preset", options.get("upload_preset")); + + if (options.get("signature") == null) { + putEager("eager", options, params); + Object transformation = options.get("transformation"); + if (transformation != null) { + if (transformation instanceof Transformation) { + transformation = ((Transformation) transformation).generate(); + } + params.put("transformation", transformation.toString()); + } + processWriteParameters(options, params); + } else { + // if there's a signature, it means all the params are already serialized so + // we don't need to construct them, just pass the value as is: + params.put("eager", (String) options.get("eager")); + params.put("transformation", (String) options.get("transformation")); + params.put("headers", (String) options.get("headers")); + params.put("tags", (String) options.get("tags")); + params.put("face_coordinates", (String) options.get("face_coordinates")); + params.put("context", (String) options.get("context")); + params.put("ocr", (String) options.get("ocr")); + params.put("raw_convert", (String) options.get("raw_convert")); + params.put("categorization", (String) options.get("categorization")); + params.put("detection", (String) options.get("detection")); + params.put("similarity_search", (String) options.get("similarity_search")); + params.put("auto_tagging", (String) options.get("auto_tagging")); + params.put("access_control", (String) options.get("access_control")); + } + return params; + } + + public static Map buildMultiParams(Map options) { + Map params = new HashMap(); + + Object transformation = options.get("transformation"); + if (transformation != null) { + if (transformation instanceof Transformation) { + transformation = ((Transformation) transformation).generate(); + } + params.put("transformation", transformation.toString()); + } + params.put("tag", options.get("tag")); + if (options.containsKey("urls")) { + params.put("urls", Arrays.asList((String[]) options.get("urls"))); + } + params.put("notification_url", (String) options.get("notification_url")); + params.put("format", (String) options.get("format")); + params.put("async", ObjectUtils.asBoolean(options.get("async"), false).toString()); + params.put("mode", options.get("mode")); + putObject("timestamp", options, params, Util.timestamp()); + + return params; + } + + public static Map buildGenerateSpriteParams(Map options) { + HashMap params = new HashMap(); + Object transParam = options.get("transformation"); + Transformation transformation = null; + if (transParam instanceof Transformation) { + transformation = new Transformation((Transformation) transParam); + } else if (transParam instanceof String) { + transformation = new Transformation().rawTransformation((String) transParam); + } else { + transformation = new Transformation(); + } + String format = (String) options.get("format"); + if (format != null) { + transformation.fetchFormat(format); + } + params.put("transformation", transformation.generate()); + params.put("tag", options.get("tag")); + if (options.containsKey("urls")) { + params.put("urls", Arrays.asList((String[]) options.get("urls"))); + } + params.put("notification_url", (String) options.get("notification_url")); + params.put("async", ObjectUtils.asBoolean(options.get("async"), false).toString()); + params.put("mode", options.get("mode")); + putObject("timestamp", options, params, Util.timestamp()); + + return params; + } + + protected static final String buildEager(List transformations) { + if (transformations == null) { + return null; + } + + List eager = new ArrayList(); + for (Transformation transformation : transformations) { + String transformationString = transformation.generate(); + if (StringUtils.isNotBlank(transformationString)) { + eager.add(transformationString); + } + } + + return StringUtils.join(eager, "|"); + } + + @SuppressWarnings("unchecked") + public static final void processWriteParameters(Map options, Map params) { + if (options.get("headers") != null) + params.put("headers", buildCustomHeaders(options.get("headers"))); + if (options.get("tags") != null) + params.put("tags", StringUtils.join(ObjectUtils.asArray(options.get("tags")), ",")); + if (options.get("face_coordinates") != null) + params.put("face_coordinates", Coordinates.parseCoordinates(options.get("face_coordinates")).toString()); + if (options.get("custom_coordinates") != null) + params.put("custom_coordinates", Coordinates.parseCoordinates(options.get("custom_coordinates")).toString()); + if (options.get("context") != null) + params.put("context", encodeContext(options.get("context"))); + if (options.get("metadata") != null) + params.put("metadata", encodeContext(options.get("metadata"))); + if (options.get("access_control") != null) { + params.put("access_control", encodeAccessControl(options.get("access_control"))); + } + if (options.get("asset_folder") != null) { + params.put("asset_folder", options.get("asset_folder")); + } + if (options.get("unique_display_name") != null) { + params.put("unique_display_name", options.get("unique_display_name")); + } + if (options.get("display_name") != null) { + params.put("display_name", options.get("display_name")); + } + putObject("ocr", options, params); + putObject("raw_convert", options, params); + putObject("categorization", options, params); + putObject("detection", options, params); + putObject("similarity_search", options, params); + putObject("background_removal", options, params); + if (options.get("auto_tagging") != null) { + params.put("auto_tagging", ObjectUtils.asFloat(options.get("auto_tagging"))); + } + if (options.get("clear_invalid") != null) { + params.put("clear_invalid", options.get("clear_invalid")); + } + if(options.get("visual_search") != null) { + params.put("visual_search", options.get("visual_search")); + } + if(options.get("auto_chaptering") != null) { + params.put("auto_chaptering", options.get("auto_chaptering")); + } + if(options.get("auto_transcription") != null) { + params.put("auto_transcription", options.get("auto_transcription")); + } + } + + protected static String encodeAccessControl(Object accessControl) { + if (accessControl instanceof AccessControlRule) { + accessControl = Arrays.asList(accessControl); + } + + return JSONObject.wrap(accessControl).toString(); + } + + protected static String encodeContext(Object context) { + if (context instanceof Map) { + Map mapArg = (Map) context; + HashSet out = new HashSet(); + for (Map.Entry entry : mapArg.entrySet()) { + final String value; + if (entry.getValue() instanceof List) { + value = encodeList(((List) entry.getValue()).toArray()); + } else if (entry.getValue() instanceof String[]) { + value = encodeList((String[]) entry.getValue()); + } else { + value = entry.getValue().toString(); + } + out.add(entry.getKey() + "=" + encodeSingleContextString(value)); + } + return StringUtils.join(out.toArray(), "|"); + } else if (context == null) { + return null; + } else { + return context.toString(); + } + } + + private static String encodeList(Object[] list) { + StringBuilder builder = new StringBuilder("["); + + boolean first = true; + for (Object s : list) { + if (!first) { + builder.append(","); + } + + builder.append("\"").append(encodeSingleContextString(s.toString())).append("\""); + first = false; + } + + return builder.append("]").toString(); + } + + private static String encodeSingleContextString(String value) { + return value.replaceAll("([=\\|])", "\\\\$1"); + } + + @SuppressWarnings("unchecked") + protected static final String buildCustomHeaders(Object headers) { + if (headers == null) { + return null; + } else if (headers instanceof String) { + return (String) headers; + } else if (headers instanceof Object[]) { + return StringUtils.join((Object[]) headers, "\n") + "\n"; + } else { + Map headersMap = (Map) headers; + StringBuilder builder = new StringBuilder(); + for (Map.Entry header : headersMap.entrySet()) { + builder.append(header.getKey()).append(": ").append(header.getValue()).append("\n"); + } + return builder.toString(); + } + } + + @SuppressWarnings("rawtypes") + public static void clearEmpty(Map params) { + for (Iterator iterator = params.values().iterator(); iterator.hasNext(); ) { + Object value = iterator.next(); + if (value == null || "".equals(value)) { + iterator.remove(); + } + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static final Map buildArchiveParams(Map options, String targetFormat) { + Map params = new HashMap(); + if (options != null && options.size() > 0) { + params.put("type", options.get("type")); + params.put("mode", options.get("mode")); + params.put("target_format", targetFormat); + params.put("target_public_id", options.get("target_public_id")); + putBoolean("flatten_folders", options, params); + putBoolean("flatten_transformations", options, params); + putBoolean("use_original_filename", options, params); + putBoolean("async", options, params); + putBoolean("keep_derived", options, params); + params.put("notification_url", options.get("notification_url")); + putArray("target_tags", options, params); + putArray("tags", options, params); + putArray("public_ids", options, params); + putArray("fully_qualified_public_ids", options, params); + putArray("prefixes", options, params); + putEager("transformations", options, params); + putObject("timestamp", options, params, Util.timestamp()); + putBoolean("skip_transformation_name", options, params); + putBoolean("allow_missing", options, params); + putObject("expires_at", options, params); + } + return params; + } + + private static void putEager(String name, Map from, Map to) { + final Object transformations = from.get(name); + if (transformations != null) + to.put(name, buildEager((List) transformations)); + } + + private static void putBoolean(String name, Map from, Map to) { + final Object value = from.get(name); + if (value != null) { + to.put(name, ObjectUtils.asBoolean(value)); + } + } + + private static void putObject(String name, Map from, Map to) { + putObject(name, from, to, null); + } + + private static void putObject(String name, Map from, Map to, Object defaultValue) { + final Object value = from.get(name); + if (value != null) { + to.put(name, value); + } else if (defaultValue != null) { + to.put(name, defaultValue); + } + } + + private static void putArray(String name, Map from, Map to) { + final Object value = from.get(name); + if (value != null) { + to.put(name, ObjectUtils.asArray(value)); + } + } + + protected static String timestamp() { + return Long.toString(System.currentTimeMillis() / 1000L); + } + + /** + * Encodes passed string value into a sequence of bytes using the UTF-8 charset. + * + * @param string string value to encode + * @return byte array representing passed string value + */ + public static byte[] getUTF8Bytes(String string) { + try { + return string.getBytes("UTF-8"); + } catch (java.io.UnsupportedEncodingException e) { + throw new RuntimeException("Unexpected exception", e); + } + } + + /** + * Calculates signature, or hashed message authentication code (HMAC) of provided parameters name-value pairs and + * secret value using default hashing algorithm (SHA1). + *

+ * Argument for hashing function is built by joining sorted parameter name-value pairs into single string in the + * same fashion as HTTP GET method uses, and concatenating the result with secret value in the end. Method supports + * arrays/collections as parameter values. In this case, the elements of array/collection are joined into single + * comma-delimited string prior to inclusion into the result. + * + * @param paramsToSign parameter name-value pairs list represented as instance of {@link Map} + * @param apiSecret secret value + * @return hex-string representation of signature calculated based on provided parameters map and secret + */ + public static String produceSignature(Map paramsToSign, String apiSecret, int signatureVersion) { + return produceSignature(paramsToSign, apiSecret, SignatureAlgorithm.SHA1, signatureVersion); + } + + /** + * Calculates signature, or hashed message authentication code (HMAC) of provided parameters name-value pairs and + * secret value using specified hashing algorithm. + *

+ * Argument for hashing function is built by joining sorted parameter name-value pairs into single string in the + * same fashion as HTTP GET method uses, and concatenating the result with secret value in the end. Method supports + * arrays/collections as parameter values. In this case, the elements of array/collection are joined into single + * comma-delimited string prior to inclusion into the result. + * + * @param paramsToSign parameter name-value pairs list represented as instance of {@link Map} + * @param apiSecret secret value + * @param signatureAlgorithm type of hashing algorithm to use for calculation of HMAC + * @return hex-string representation of signature calculated based on provided parameters map and secret + */ + public static String produceSignature(Map paramsToSign, String apiSecret, SignatureAlgorithm signatureAlgorithm, int signatureVersion) { + Collection flattenedParams = flattenAndSanitizeParams(paramsToSign, signatureVersion); + String toSign = StringUtils.join(flattenedParams, "&") + apiSecret; + byte[] hash = Util.hash(toSign, signatureAlgorithm); + return StringUtils.encodeHexString(hash); + } + + private static Collection flattenAndSanitizeParams(Map paramsToSign, int signatureVersion) { + Collection params = new ArrayList<>(); + + for (Map.Entry entry : new TreeMap<>(paramsToSign).entrySet()) { + Object value = entry.getValue(); + String rawValue = null; + + if (value instanceof Collection) { + rawValue = StringUtils.join((Collection) value, ","); + } else if (value instanceof Object[]) { + rawValue = StringUtils.join((Object[]) value, ","); + } else if (value != null && StringUtils.isNotBlank(value.toString())) { + rawValue = value.toString(); + } + + if (rawValue != null) { + String sanitizedValue = (signatureVersion == 2) + ? escapeAmpersand(rawValue) + : rawValue; + + params.add(entry.getKey() + "=" + sanitizedValue); + } + } + + return params; + } + + private static String escapeAmpersand(String input) { + return input.replace("&", "%26"); + } + + /** + * Computes hash from input string using specified algorithm. + * + * @param input string which to compute hash from + * @param signatureAlgorithm algorithm to use for computing hash + * @return array of bytes of computed hash value + */ + public static byte[] hash(String input, SignatureAlgorithm signatureAlgorithm) { + try { + return MessageDigest.getInstance(signatureAlgorithm.getAlgorithmId()).digest(Util.getUTF8Bytes(input)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Unexpected exception", e); + } + } } diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/ApiResponse.java b/cloudinary-core/src/main/java/com/cloudinary/api/ApiResponse.java new file mode 100644 index 00000000..1de0dfa5 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/ApiResponse.java @@ -0,0 +1,11 @@ +package com.cloudinary.api; + +import java.text.ParseException; +import java.util.Map; + +@SuppressWarnings("rawtypes") +public interface ApiResponse extends Map { + Map rateLimits() throws ParseException; + + RateLimit apiRateLimit() throws ParseException; +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/AuthorizationRequired.java b/cloudinary-core/src/main/java/com/cloudinary/api/AuthorizationRequired.java new file mode 100644 index 00000000..4d920a16 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/AuthorizationRequired.java @@ -0,0 +1,11 @@ +package com.cloudinary.api; + +import com.cloudinary.api.exceptions.ApiException; + +public class AuthorizationRequired extends ApiException { + private static final long serialVersionUID = 7160740370855761014L; + + public AuthorizationRequired(String message) { + super(message); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/RateLimit.java b/cloudinary-core/src/main/java/com/cloudinary/api/RateLimit.java new file mode 100644 index 00000000..da44da34 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/RateLimit.java @@ -0,0 +1,37 @@ +package com.cloudinary.api; + +import java.util.Date; + +public class RateLimit { + private long limit = 0L; + private long remaining = 0L; + private Date reset = null; + + public RateLimit() { + super(); + } + + public long getLimit() { + return limit; + } + + public void setLimit(long limit) { + this.limit = limit; + } + + public long getRemaining() { + return remaining; + } + + public void setRemaining(long remaining) { + this.remaining = remaining; + } + + public Date getReset() { + return reset; + } + + public void setReset(Date reset) { + this.reset = reset; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/AlreadyExists.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/AlreadyExists.java new file mode 100644 index 00000000..e4ce2729 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/AlreadyExists.java @@ -0,0 +1,9 @@ +package com.cloudinary.api.exceptions; + +public class AlreadyExists extends ApiException { + private static final long serialVersionUID = 999568182896607322L; + + public AlreadyExists(String message) { + super(message); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/ApiException.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/ApiException.java new file mode 100644 index 00000000..e1ca0f3c --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/ApiException.java @@ -0,0 +1,9 @@ +package com.cloudinary.api.exceptions; + +public class ApiException extends Exception { + private static final long serialVersionUID = 4416861825144420038L; + + public ApiException(String message) { + super(message); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/BadRequest.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/BadRequest.java new file mode 100644 index 00000000..a57f75a1 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/BadRequest.java @@ -0,0 +1,10 @@ +package com.cloudinary.api.exceptions; + + +public class BadRequest extends ApiException { + private static final long serialVersionUID = 1410136354253339531L; + + public BadRequest(String message) { + super(message); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/GeneralError.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/GeneralError.java new file mode 100644 index 00000000..34992a81 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/GeneralError.java @@ -0,0 +1,9 @@ +package com.cloudinary.api.exceptions; + +public class GeneralError extends ApiException { + private static final long serialVersionUID = 4553362706625067182L; + + public GeneralError(String message) { + super(message); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/NotAllowed.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/NotAllowed.java new file mode 100644 index 00000000..cad00f98 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/NotAllowed.java @@ -0,0 +1,10 @@ +package com.cloudinary.api.exceptions; + + +public class NotAllowed extends ApiException { + private static final long serialVersionUID = 4371365822491647653L; + + public NotAllowed(String message) { + super(message); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/NotFound.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/NotFound.java new file mode 100644 index 00000000..1c93692d --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/NotFound.java @@ -0,0 +1,10 @@ +package com.cloudinary.api.exceptions; + + +public class NotFound extends ApiException { + private static final long serialVersionUID = -2072640462778940357L; + + public NotFound(String message) { + super(message); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/RateLimited.java b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/RateLimited.java new file mode 100644 index 00000000..0afe2394 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/exceptions/RateLimited.java @@ -0,0 +1,10 @@ +package com.cloudinary.api.exceptions; + + +public class RateLimited extends ApiException { + private static final long serialVersionUID = -8298038106172355219L; + + public RateLimited(String message) { + super(message); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/ApiResponseSignatureVerifier.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/ApiResponseSignatureVerifier.java new file mode 100644 index 00000000..f6d7da67 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/ApiResponseSignatureVerifier.java @@ -0,0 +1,65 @@ +package com.cloudinary.api.signing; + +import com.cloudinary.SignatureAlgorithm; +import com.cloudinary.Util; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import static com.cloudinary.utils.StringUtils.emptyIfNull; + +/** + * The {@code ApiResponseSignatureVerifier} class is responsible for verifying Cloudinary Upload API response signatures. + */ +public class ApiResponseSignatureVerifier { + private final String secretKey; + private final SignatureAlgorithm signatureAlgorithm; + + /** + * Initializes new instance of {@code ApiResponseSignatureVerifier} class with a secret key required to perform + * API response signatures verification. + * + * @param secretKey shared secret key string which is used to sign and verify authenticity of API responses + */ + public ApiResponseSignatureVerifier(String secretKey) { + if (StringUtils.isBlank(secretKey)) { + throw new IllegalArgumentException("Secret key is required"); + } + + this.secretKey = secretKey; + this.signatureAlgorithm = SignatureAlgorithm.SHA1; + } + + /** + * Initializes new instance of {@code ApiResponseSignatureVerifier} class with a secret key required to perform + * API response signatures verification. + * + * @param secretKey shared secret key string which is used to sign and verify authenticity of API responses + * @param signatureAlgorithm type of hashing algorithm to use for calculation of HMACs + */ + public ApiResponseSignatureVerifier(String secretKey, SignatureAlgorithm signatureAlgorithm) { + if (StringUtils.isBlank(secretKey)) { + throw new IllegalArgumentException("Secret key is required"); + } + + this.secretKey = secretKey; + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * Checks whether particular Cloudinary Upload API response signature matches expected signature. + * + * The task is performed by generating signature using same hashing algorithm as used on Cloudinary servers and + * comparing the result with provided actual signature. + * + * @param publicId public id of uploaded resource as stated in upload API response + * @param version version of uploaded resource as stated in upload API response + * @param signature signature of upload API response, usually passed via X-Cld-Signature custom HTTP response header + * + * @return true if response signature passed verification procedure + */ + public boolean verifySignature(String publicId, String version, String signature) { + return Util.produceSignature(ObjectUtils.asMap( + "public_id", emptyIfNull(publicId), + "version", emptyIfNull(version)), secretKey, signatureAlgorithm, 1).equals(signature); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifier.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifier.java new file mode 100644 index 00000000..1b5d3e89 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifier.java @@ -0,0 +1,75 @@ +package com.cloudinary.api.signing; + +import com.cloudinary.SignatureAlgorithm; + +import static com.cloudinary.utils.StringUtils.emptyIfNull; + +/** + * The {@code NotificationRequestSignatureVerifier} class is responsible for verifying authenticity and integrity + * of Cloudinary Upload notifications. + */ +public class NotificationRequestSignatureVerifier { + private final SignedPayloadValidator signedPayloadValidator; + + /** + * Initializes new instance of {@code NotificationRequestSignatureVerifier} with secret key value. + * + * @param secretKey shared secret key string which is used to sign and verify authenticity of notifications + */ + public NotificationRequestSignatureVerifier(String secretKey) { + this.signedPayloadValidator = new SignedPayloadValidator(secretKey, SignatureAlgorithm.SHA1); + } + + /** + * Initializes new instance of {@code NotificationRequestSignatureVerifier} with secret key value. + * + * @param secretKey shared secret key string which is used to sign and verify authenticity of notifications + * @param signatureAlgorithm type of hashing algorithm to use for calculation of HMACs + */ + public NotificationRequestSignatureVerifier(String secretKey, SignatureAlgorithm signatureAlgorithm) { + this.signedPayloadValidator = new SignedPayloadValidator(secretKey, signatureAlgorithm); + } + + /** + * Verifies signature of Cloudinary Upload notification. + * + * @param body notification message body, represented as string + * @param timestamp value of X-Cld-Timestamp custom HTTP header of notification message, representing notification + * issue timestamp + * @param signature actual signature value, usually passed via X-Cld-Signature custom HTTP header of notification + * message + * @return true if notification passed verification procedure + */ + public boolean verifySignature(String body, String timestamp, String signature) { + return signedPayloadValidator.validateSignedPayload( + emptyIfNull(body) + emptyIfNull(timestamp), + signature); + } + + /** + * Verifies signature of Cloudinary Upload notification. + *

+ * Differs from {@link #verifySignature(String, String, String)} in additional validation which consists of making + * sure the notification being verified is still not expired based on timestamp parameter value. + * + * @param body notification message body, represented as string + * @param timestamp value of X-Cld-Timestamp custom HTTP header of notification message, representing notification + * issue timestamp in seconds + * @param signature actual signature value, usually passed via X-Cld-Signature custom HTTP header of notification + * message + * @param secondsValidFor the amount of time, in seconds, the notification message is considered valid by client + * @return true if notification passed verification procedure + */ + public boolean verifySignature(String body, String timestamp, String signature, long secondsValidFor) { + long parsedTimestamp; + try { + parsedTimestamp = Long.parseLong(timestamp); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Provided timestamp is not a valid number", e); + } + + return verifySignature(body, timestamp, signature) && + (System.currentTimeMillis() / 1000L - parsedTimestamp <= secondsValidFor); + } + +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/SignedPayloadValidator.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/SignedPayloadValidator.java new file mode 100644 index 00000000..771bdbe0 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/SignedPayloadValidator.java @@ -0,0 +1,29 @@ +package com.cloudinary.api.signing; + +import com.cloudinary.SignatureAlgorithm; +import com.cloudinary.Util; +import com.cloudinary.utils.StringUtils; + +import static com.cloudinary.utils.StringUtils.emptyIfNull; + +class SignedPayloadValidator { + private final String secretKey; + private final SignatureAlgorithm signatureAlgorithm; + + SignedPayloadValidator(String secretKey, SignatureAlgorithm signatureAlgorithm) { + if (StringUtils.isBlank(secretKey)) { + throw new IllegalArgumentException("Secret key is required"); + } + + this.secretKey = secretKey; + this.signatureAlgorithm = signatureAlgorithm; + } + + boolean validateSignedPayload(String signedPayload, String signature) { + String expectedSignature = + StringUtils.encodeHexString(Util.hash(emptyIfNull(signedPayload) + secretKey, + signatureAlgorithm)); + + return expectedSignature.equals(signature); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/api/signing/package-info.java b/cloudinary-core/src/main/java/com/cloudinary/api/signing/package-info.java new file mode 100644 index 00000000..9c75e3bd --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/api/signing/package-info.java @@ -0,0 +1,7 @@ +/** + * The package holds classes used internally to implement verification procedures of authenticity and integrity of + * client communication with Cloudinary servers. Verification is in most cases based on calculating and comparing so called + * signatures, or hashed message authentication codes (HMAC) - string values calculated based on message payload, some + * secret key value shared between communicating parties and agreed hashing function. + */ +package com.cloudinary.api.signing; \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/DateMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/DateMetadataField.java new file mode 100644 index 00000000..a4df9091 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/DateMetadataField.java @@ -0,0 +1,40 @@ +package com.cloudinary.metadata; + +import com.cloudinary.utils.ObjectUtils; + +import java.text.ParseException; +import java.util.Date; + +/** + * Represents a metadata field with type 'date' + */ +public class DateMetadataField extends MetadataField { + + public DateMetadataField() { + super(MetadataFieldType.DATE); + } + + /** + * Sets the default date used for this field. + * @param defaultValue The date to set. Date only without a time component, UTC assumed. + */ + @Override + public void setDefaultValue(Date defaultValue) { + put(DEFAULT_VALUE, ObjectUtils.toISO8601DateOnly(defaultValue)); + } + + /** + * Get the default value of this date field. + * @return The date only without a time component, UTC. + * @throws ParseException When the underlying value is malformed. + */ + @Override + public Date getDefaultValue() throws ParseException { + Object value = get(DEFAULT_VALUE); + if (value == null) { + return null; + } + + return ObjectUtils.fromISO8601DateOnly(value.toString()); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/EnumMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/EnumMetadataField.java new file mode 100644 index 00000000..79f501c3 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/EnumMetadataField.java @@ -0,0 +1,10 @@ +package com.cloudinary.metadata; + +/** + * Represents a metadata field with 'Enum' type. + */ +public class EnumMetadataField extends MetadataField { + EnumMetadataField() { + super(MetadataFieldType.ENUM); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/IntMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/IntMetadataField.java new file mode 100644 index 00000000..23510210 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/IntMetadataField.java @@ -0,0 +1,10 @@ +package com.cloudinary.metadata; + +/** + * Represents a metadata field with 'Int' type. + */ +public class IntMetadataField extends MetadataField { + public IntMetadataField() { + super(MetadataFieldType.INTEGER); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataDataSource.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataDataSource.java new file mode 100644 index 00000000..043556cd --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataDataSource.java @@ -0,0 +1,70 @@ +package com.cloudinary.metadata; + +import org.cloudinary.json.JSONArray; +import org.cloudinary.json.JSONObject; + +import java.util.List; + +/** + * Represent a data source for a given field. This is used in both 'Set' and 'Enum' field types. + * The datasource holds a list of the valid values to be used with the corresponding metadata field. + */ +public class MetadataDataSource extends JSONObject { + /** + * Creates a new instance of data source with the given list of entries. + * @param entries + */ + public MetadataDataSource(List entries) { + put("values", new JSONArray(entries.toArray())); + } + + /** + * Represents a single entry in a datasource definition for a field. + */ + public static class Entry extends JSONObject { + public Entry(String externalId, String value){ + setExternalId(externalId); + setValue(value); + } + + /** + * Create a new entry with a string value. + * @param value The value to use in the entry. + */ + public Entry(String value){ + this(null, value); + } + + /** + * Set the id of the entry. Will be auto-generated if left blank. + * @param externalId + */ + public void setExternalId(String externalId) { + put("external_id", externalId); + } + + /** + * Get the id of the entry. + * @return + */ + public String getExternalId() { + return optString("external_id"); + } + + /** + * Set the value of the entry. + * @param value The value to set. + */ + public void setValue(String value) { + put("value", value); + } + + /** + * Get the value of the entry. + * @return The value. + */ + public String getValue() { + return optString("value"); + } + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataField.java new file mode 100644 index 00000000..7fe81c43 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataField.java @@ -0,0 +1,158 @@ +package com.cloudinary.metadata; + +import org.cloudinary.json.JSONObject; + +import java.text.ParseException; + +/** + * Represents a single metadata field. Use one of the derived classes in the metadata API calls. + * @param + */ +public class MetadataField extends JSONObject { + + public static final String DEFAULT_VALUE = "default_value"; + public static final String EXTERNAL_ID = "external_id"; + public static final String LABEL = "label"; + public static final String MANDATORY = "mandatory"; + public static final String TYPE = "type"; + public static final String VALIDATION = "validation"; + public static final String RESTRICTIONS = "restrictions"; + public static final String DEFAULT_DISABLED = "default_disabled"; + public static final String ALLOW_DYNAMIC_LIST_VALUES = "allow_dynamic_list_values"; + + public MetadataField(MetadataFieldType type) { + put(TYPE, type.toString()); + } + + public MetadataField(String type) { + put(TYPE, type); + } + + /** + * The type of the field. + * @return String with the name of the type. + */ + public MetadataFieldType getType() { + return MetadataFieldType.valueOf(optString(TYPE).toUpperCase()); + } + + /** + * Get the id of the field. + * @return String, field id. + */ + public String getExternalId() { + return optString(EXTERNAL_ID); + } + + /** + * Set the id of the string (auto-generated if this is left blank). + * @param externalId The id to set. + */ + public void setExternalId(String externalId) { + put(EXTERNAL_ID, externalId); + } + + /** + * Get the label of the field + * @return String, the label of the field. + */ + public String getLabel() { + return optString(LABEL); + } + + /** + * Sets the label of the field + * @param label The label to set. + */ + public void setLabel(String label) { + put(LABEL, label); + } + + /** + * Cehcks whether the field is mandatory. + * @return Boolean indicating whether the field is mandatory. + */ + public boolean isMandatory() { + return optBoolean(MANDATORY); + } + + /** + * Sets a boolean indicating whether this fields needs to be mandatory. + * @param mandatory The boolean to set. + */ + public void setMandatory(Boolean mandatory) { + put(MANDATORY, mandatory); + } + + /** + * Gets the default value of this field. + * @return The default value + * @throws ParseException If the stored value can't be parsed to the correct type. + */ + public T getDefaultValue() throws ParseException { + //noinspection unchecked + return (T)opt(DEFAULT_VALUE); + } + + /** + * Set the default value of the field + * @param defaultValue The value to set. + */ + public void setDefaultValue(T defaultValue) { + put(DEFAULT_VALUE, defaultValue); + } + + /** + * Get the validation rules of this field. + * @return The validation rules. + */ + public MetadataValidation getValidation() { + return (MetadataValidation) optJSONObject(VALIDATION); + } + + /** + * Set the validation rules of this field. + * @param validation The rules to set. + */ + public void setValidation(MetadataValidation validation) { + put(VALIDATION, validation); + } + + /** + * Get the data source definition of this field. + * @return The data source. + */ + public MetadataDataSource getDataSource() { + return (MetadataDataSource) optJSONObject("datasource"); + } + + /** + * Set the datasource for the field. + * @param dataSource The datasource to set. + */ + public void setDataSource(MetadataDataSource dataSource) { + put("datasource", dataSource); + } + + /** + * Set the restrictions rules of this field. + * @param restrictions The rules to set. + */ + public void setRestrictions(Restrictions restrictions) { + put(RESTRICTIONS, restrictions.toHash()); + } + + /** + * Set the value indicating whether the field should be disabled by default + * @param disabled The value to set. + */ + public void setDefaultDisabled(Boolean disabled) { + put(DEFAULT_DISABLED, disabled); + } + + /** + * Set the value indicating whether the dynamic list values should allow + * @param allowDynamicListValues The value to set. + */ + public void setAllowDynamicListValues(Boolean allowDynamicListValues) {put(ALLOW_DYNAMIC_LIST_VALUES, allowDynamicListValues);} +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataFieldType.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataFieldType.java new file mode 100644 index 00000000..34362f27 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataFieldType.java @@ -0,0 +1,17 @@ +package com.cloudinary.metadata; + +/** + * Enum represneting all the valid field types. + */ +public enum MetadataFieldType { + STRING, + INTEGER, + DATE, + ENUM, + SET; + + @Override + public String toString() { + return super.toString().toLowerCase(); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRule.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRule.java new file mode 100644 index 00000000..4df82ded --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRule.java @@ -0,0 +1,65 @@ +package com.cloudinary.metadata; + +import com.cloudinary.utils.ObjectUtils; + +import java.util.HashMap; +import java.util.Map; + +public class MetadataRule { + String metadataFieldId; + String name; + MetadataRuleCondition condition; + MetadataRuleResult result; + + public MetadataRule(String metadataFieldId, String name, MetadataRuleCondition condition, MetadataRuleResult result) { + this.metadataFieldId = metadataFieldId; + this.name = name; + this.condition = condition; + this.result = result; + } + + public String getMetadataFieldId() { + return metadataFieldId; + } + + public void setMetadataFieldId(String metadataFieldId) { + this.metadataFieldId = metadataFieldId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public MetadataRuleCondition getCondition() { + return condition; + } + + public void setCondition(MetadataRuleCondition condition) { + this.condition = condition; + } + + public MetadataRuleResult getResult() { + return result; + } + + public void setResult(MetadataRuleResult result) { + this.result = result; + } + + public Map asMap() { + Map map = new HashMap(); + map.put("metadata_field_id", getMetadataFieldId()); + map.put("name", getName()); + if (getCondition() != null) { + map.put("condition", ObjectUtils.toJSON(getCondition().asMap())); + } + if(getResult() != null) { + map.put("result", ObjectUtils.toJSON(getResult().asMap())); + } + return map; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRuleCondition.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRuleCondition.java new file mode 100644 index 00000000..55e3a714 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRuleCondition.java @@ -0,0 +1,58 @@ +package com.cloudinary.metadata; +import java.util.HashMap; +import java.util.Map; + +public class MetadataRuleCondition { + String metadata_field_id; + Boolean populated; + Map includes; + String equals; + + public MetadataRuleCondition(String metadata_field_id, Boolean populated, Map includes, String equals) { + this.metadata_field_id = metadata_field_id; + this.populated = populated; + this.includes = includes; + this.equals = equals; + } + + public String getMetadata_field_id() { + return metadata_field_id; + } + + public void setMetadata_field_id(String metadata_field_id) { + this.metadata_field_id = metadata_field_id; + } + + public Boolean getPopulated() { + return populated; + } + + public void setPopulated(Boolean populated) { + this.populated = populated; + } + + public Map getIncludes() { + return includes; + } + + public void setIncludes(Map includes) { + this.includes = includes; + } + + public String getEquals() { + return equals; + } + + public void setEquals(String equals) { + this.equals = equals; + } + + public Map asMap() { + Map result = new HashMap(4); + result.put("metadata_field_id", metadata_field_id); + result.put("populated", populated); + result.put("includes", includes); + result.put("equals", equals); + return result; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRuleResult.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRuleResult.java new file mode 100644 index 00000000..2d4efff0 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataRuleResult.java @@ -0,0 +1,58 @@ +package com.cloudinary.metadata; + +import java.util.HashMap; +import java.util.Map; + +public class MetadataRuleResult { + Boolean enabled; + String activateValues; + String applyValues; + Boolean setMandatory; + + public MetadataRuleResult(Boolean enabled, String activateValues, String applyValues, Boolean setMandatory) { + this.enabled = enabled; + this.activateValues = activateValues; + this.applyValues = applyValues; + this.setMandatory = setMandatory; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public String getActivateValues() { + return activateValues; + } + + public void setActivateValues(String activateValues) { + this.activateValues = activateValues; + } + + public String getApplyValues() { + return applyValues; + } + + public void setApplyValues(String applyValues) { + this.applyValues = applyValues; + } + + public Boolean getSetMandatory() { + return setMandatory; + } + + public void setSetMandatory(Boolean setMandatory) { + this.setMandatory = setMandatory; + } + public Map asMap() { + Map result = new HashMap(4); + result.put("enable", enabled); + result.put("activate_values", activateValues); + result.put("apply_values", applyValues); + result.put("mandatory", setMandatory); + return result; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataValidation.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataValidation.java new file mode 100644 index 00000000..f38b732e --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/MetadataValidation.java @@ -0,0 +1,177 @@ +package com.cloudinary.metadata; + +import com.cloudinary.utils.ObjectUtils; +import org.cloudinary.json.JSONArray; +import org.cloudinary.json.JSONObject; + +import java.util.Date; +import java.util.List; + +/** + * Represents the base class for metadata fields validation mechanisms. + */ +public abstract class MetadataValidation extends JSONObject { + + public static final String TYPE = "type"; + public static final String MIN = "min"; + public static final String MAX = "max"; + public static final String STRLEN = "strlen"; + public static final String EQUALS = "equals"; + public static final String GREATER_THAN = "greater_than"; + public static final String LESS_THAN = "less_than"; + public static final String VALUE = "value"; + + /** + * An 'And' rule validation used to combine other rules with an 'AND' logic relation between them. + */ + public static class AndValidator extends MetadataValidation { + + public static final String AND = "and"; + + /** + * Create a new instance of the validator with the given rules. + * @param rules The rules to use. + */ + public AndValidator(List rules) { + put(TYPE, AND); + put("rules", new JSONArray(rules.toArray())); + } + } + + /** + * A validator to validate string lengths + */ + public static class StringLength extends MetadataValidation { + /** + * Create a new instance with the given min and max. + * @param min Minimum valid string length. + * @param max Maximum valid string length. + */ + public StringLength(Integer min, Integer max) { + put(TYPE, STRLEN); + put(MIN, min); + put(MAX, max); + } + } + + /** + * Base class for all comparison (greater than/less than) validation rules. + * @param + */ + public abstract static class ComparisonRule extends MetadataValidation { + public ComparisonRule(String type, T value) { + this(type, value, null); + } + + public ComparisonRule(String type, T value, Boolean equals) { + put(TYPE, type); + putValue(value); + if (equals != null) { + put(EQUALS, equals); + } + } + + protected void putValue(T value) { + put(VALUE, value); + } + } + + /** + * Great-than rule for integers. + */ + public static class IntGreaterThan extends ComparisonRule { + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule + */ + public IntGreaterThan(Integer value) { + super(GREATER_THAN, value); + } + + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public IntGreaterThan(Integer value, Boolean equals) { + super(GREATER_THAN, value, equals); + } + } + + /** + * Great-than rule for dates. + */ + public static class DateGreaterThan extends ComparisonRule { + /** + * Create a new rule with the given date. + * @param value The integer to reference in the rule + */ + public DateGreaterThan(Date value) { + super(GREATER_THAN, value); + } + + /** + * Create a new rule with the given date. + * @param value The date to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public DateGreaterThan(Date value, Boolean equals) { + super(GREATER_THAN, value, equals); + } + + @Override + protected void putValue(Date value) { + put(VALUE, ObjectUtils.toISO8601DateOnly(value)); + } + } + + /** + * Less-than rule for integers. + */ + public static class IntLessThan extends ComparisonRule { + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule + */ + public IntLessThan(Integer value) { + super(LESS_THAN, value); + } + + /** + * Create a new rule with the given integer. + * @param value The integer to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public IntLessThan(Integer value, Boolean equals) { + super(LESS_THAN, value, equals); + } + } + + /** + * Less-than rule for dates. + */ + public static class DateLessThan extends ComparisonRule { + /** + * Create a new rule with the given date. + * @param value The integer to reference in the rule + */ + public DateLessThan(Date value) { + super(LESS_THAN, value); + } + + /** + * Create a new rule with the given date. + * @param value The date to reference in the rule. + * @param equals Whether a field value equal to the rule value is considered valid. + */ + public DateLessThan(Date value, Boolean equals) { + super(LESS_THAN, value, equals); + } + + @Override + protected void putValue(Date value) { + put(VALUE, ObjectUtils.toISO8601DateOnly(value)); + } + } +} + diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/Restrictions.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/Restrictions.java new file mode 100644 index 00000000..25d80d12 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/Restrictions.java @@ -0,0 +1,40 @@ +package com.cloudinary.metadata; + +import java.util.HashMap; + +/** + * Represents the restrictions metadata field. + */ +public class Restrictions { + + private final HashMap restrictions = new HashMap(); + + /** + * Set the custom field into restrictions. + * @param key The key of the field. + * @param value The value of the field. + */ + public Restrictions setRestriction(String key, Object value) { + restrictions.put(key, value); + return this; + } + + /** + * Set the read only ui field. + * @param value The read only ui value. + */ + public Restrictions setReadOnlyUI(Boolean value) { + return setRestriction("readonly_ui", value); + } + + /** + * Set the read only ui field to true. + */ + public Restrictions setReadOnlyUI() { + return this.setReadOnlyUI(true); + } + + public HashMap toHash() { + return restrictions; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/SetMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/SetMetadataField.java new file mode 100644 index 00000000..48d54823 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/SetMetadataField.java @@ -0,0 +1,12 @@ +package com.cloudinary.metadata; + +import java.util.List; + +/** + * Represents a metadata field with 'Set' type. + */ +public class SetMetadataField extends MetadataField> { + public SetMetadataField() { + super(MetadataFieldType.SET); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/metadata/StringMetadataField.java b/cloudinary-core/src/main/java/com/cloudinary/metadata/StringMetadataField.java new file mode 100644 index 00000000..e7e03405 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/metadata/StringMetadataField.java @@ -0,0 +1,10 @@ +package com.cloudinary.metadata; + +/** + * Represents a metadata field with 'String' type. + */ +public class StringMetadataField extends MetadataField { + public StringMetadataField() { + super(MetadataFieldType.STRING); + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/provisioning/Account.java b/cloudinary-core/src/main/java/com/cloudinary/provisioning/Account.java new file mode 100644 index 00000000..1c545345 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/provisioning/Account.java @@ -0,0 +1,739 @@ +package com.cloudinary.provisioning; + +import com.cloudinary.Api; +import com.cloudinary.Cloudinary; +import com.cloudinary.Util; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.utils.Base64Coder; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import java.util.*; + +/** + * Entry point class for all account and provisioning API actions: Manage users, cloud names and user groups. + */ +public class Account { + private static final String CLOUDINARY_ACCOUNT_URL = "CLOUDINARY_ACCOUNT_URL"; + public static final String PROVISIONING = "provisioning"; + public static final String ACCOUNTS = "accounts"; + public static final String SUB_ACCOUNTS = "sub_accounts"; + public static final String USERS = "users"; + public static final String USER_GROUPS = "user_groups"; + public static final String ACCESS_KEYS = "access_keys"; + + private final AccountConfiguration configuration; + private final String accountId; + private final String key; + private final String secret; + private final Api api; + + /** + * Create a new instance to use the account API. The account information will be extracted from + * an environment variable CLOUDINARY_ACCOUNT_URL. If it's missing an exception will be thrown. + * + * @param cloudinary A cloudinary instance. This is used to fetch the correct network configuration. + */ + public Account(Cloudinary cloudinary) { + String provisioningData = System.getProperty(CLOUDINARY_ACCOUNT_URL, System.getenv(CLOUDINARY_ACCOUNT_URL)); + if (provisioningData != null) { + this.configuration = AccountConfiguration.from(provisioningData); + this.accountId = configuration.accountId; + this.key = configuration.provisioningApiKey; + this.secret = configuration.provisioningApiSecret; + } else { + throw new IllegalArgumentException("Must provide configuration instance or set an ENV variable: " + + "CLOUDINARY_ACCOUNT_URL=account://provisioning_api_key:provisioning_api_secret@account_id"); + } + + this.api = cloudinary.api(); + } + + /** + * Create a new instance to use the account API. The account information will be extracted from + * + * @param accountConfiguration Account configuration to use in requests. + * @param cloudinary A cloudinary instance. This is used to fetch the correct network configuration. + */ + public Account(AccountConfiguration accountConfiguration, Cloudinary cloudinary) { + this.configuration = accountConfiguration; + this.api = cloudinary.api(); + this.accountId = accountConfiguration.accountId; + this.key = accountConfiguration.provisioningApiKey; + this.secret = accountConfiguration.provisioningApiSecret; + } + + private ApiResponse callAccountApi(Api.HttpMethod method, List uri, Map params, Map options) throws Exception { + options = verifyOptions(options); + + if (options.containsKey("provisioning_api_key")){ + if (!options.containsKey("provisioning_api_secret")){ + throw new IllegalArgumentException("When providing key or secret through options, both must be provided"); + } + } else { + if (options.containsKey("provisioning_api_secret")){ + throw new IllegalArgumentException("When providing key or secret through options, both must be provided"); + } + options.put("provisioning_api_key", key); + options.put("provisioning_api_secret", secret); + } + + Util.clearEmpty(params); + + if (options == null) { + options = ObjectUtils.emptyMap(); + } + + String prefix = ObjectUtils.asString(options.get("upload_prefix"), "https://api.cloudinary.com"); + String apiKey = ObjectUtils.asString(options.get("provisioning_api_key")); + if (apiKey == null) throw new IllegalArgumentException("Must supply provisioning_api_key"); + String apiSecret = ObjectUtils.asString(options.get("provisioning_api_secret")); + if (apiSecret == null) throw new IllegalArgumentException("Must supply provisioning_api_secret"); + + String apiUrl = StringUtils.join(Arrays.asList(prefix, "v1_1"), "/"); + for (String component : uri) { + apiUrl = apiUrl + "/" + component; + } + + String authorizationHeader = getAuthorizationHeaderValue(apiKey, apiSecret, null); + + return api.getStrategy().callAccountApi(method, apiUrl, params, options, authorizationHeader); + } + + /** + * A user role to use in the user management API (create/update user). + */ + public enum Role { + MASTER_ADMIN("master_admin"), + ADMIN("admin"), + TECHNICAL_ADMIN("technical_admin"), + BILLING("billing"), + REPORTS("reports"), + MEDIA_LIBRARY_ADMIN("media_library_admin"), + MEDIA_LIBRARY_USER("media_library_user"); + + private final String serializedValue; + + Role(String serializedValue) { + this.serializedValue = serializedValue; + } + + @Override + public String toString() { + return serializedValue; + } + } + + // Sub accounts + /** + * Get details of a specific sub account + * + * @param subAccountId The id of the sub account + * @return the sub account details. + * @throws Exception If the request fails + */ + public ApiResponse subAccount(String subAccountId) throws Exception { + return subAccount(subAccountId, Collections.emptyMap()); + } + + /** + * Get details of a specific sub account + * + * @param subAccountId The id of the sub account + * @param options Generic advanced options map, see online documentation. + * @return the sub account details. + * @throws Exception If the request fails + */ + public ApiResponse subAccount(String subAccountId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, "sub_accounts", subAccountId); + return callAccountApi(Api.HttpMethod.GET, uri, Collections.emptyMap(), options); + } + + /** + * Get a list of sub accounts. + * + * @param enabled Optional. Whether to fetch enabled or disabled accounts. Default is all. + * @param ids Optional. List of sub-account IDs. Up to 100. When provided, other filters are ignored. + * @param prefix Optional. Search by prefix of the sub-account name. Case-insensitive. + * @return the list of sub-accounts details. + * @throws Exception If the request fails + */ + public ApiResponse subAccounts(Boolean enabled, List ids, String prefix) throws Exception { + return subAccounts(enabled, ids, prefix, Collections.emptyMap()); + } + + /** + * Get a list of sub accounts. + * + * @param enabled Optional. Whether to fetch enabled or disabled accounts. Default is all. + * @param ids Optional. List of sub-account IDs. Up to 100. When provided, other filters are ignored. + * @param prefix Optional. Search by prefix of the sub-account name. Case-insensitive. + * @param options Generic advanced options map, see online documentation. + * @return the list of sub-accounts details. + * @throws Exception If the request fails + */ + public ApiResponse subAccounts(Boolean enabled, List ids, String prefix, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, "sub_accounts"); + return callAccountApi(Api.HttpMethod.GET, uri, + ObjectUtils.asMap("accountId", accountId, "enabled", enabled, "ids", ids, "prefix", prefix), options); + } + + /** + * @param name Required. The name displayed in the management console. + * @param cloudName Optional, unique (case insensitive) + * @param customAttributes Custom attributes associated with the sub-account, as a map of key/value pairs. + * @param enabled Optional. Whether to create the account as enabled (default is enabled). + * @param baseAccount Optional. ID of sub-account from which to copy settings + * @return details of the created sub-account + * @throws Exception If the request fails + */ + public ApiResponse createSubAccount(String name, String cloudName, Map customAttributes, boolean enabled, String baseAccount) throws Exception { + return createSubAccount(name, cloudName, customAttributes, enabled, baseAccount, Collections.emptyMap()); + } + + /** + * @param name Required. The name displayed in the management console. + * @param cloudName Optional, unique (case insensitive) + * @param customAttributes Custom attributes associated with the sub-account, as a map of key/value pairs. + * @param enabled Optional. Whether to create the account as enabled (default is enabled). + * @param baseAccount Optional. ID of sub-account from which to copy settings + * @param options Generic advanced options map, see online documentation. + * @return details of the created sub-account + * @throws Exception If the request fails + */ + public ApiResponse createSubAccount(String name, String cloudName, Map customAttributes, boolean enabled, String baseAccount, Map options) throws Exception { + options = verifyOptions(options); + options.put("content_type", "json"); + + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, "sub_accounts"); + + return callAccountApi(Api.HttpMethod.POST, uri, ObjectUtils.asMap( + "cloud_name", cloudName, + "name", name, + "custom_attributes", customAttributes, + "enabled", enabled, + "base_sub_account_id", baseAccount), + options); + } + + /** + * @param subAccountId The id of the sub-account to update + * @param name The name displayed in the management console. + * @param cloudName The cloud name to set. + * @param customAttributes ACustom attributes associated with the sub-account, as a map of key/value pairs. + * @param enabled Set the sub-account as enabled or not. + * @return details of the updated sub-account + * @throws Exception If the request fails + */ + public ApiResponse updateSubAccount(String subAccountId, String name, String cloudName, Map customAttributes, Boolean enabled) throws Exception { + return updateSubAccount(subAccountId, name, cloudName, customAttributes, enabled, Collections.emptyMap()); + } + + /** + * @param subAccountId The id of the sub-account to update + * @param name The name displayed in the management console. + * @param cloudName The cloud name to set. + * @param customAttributes ACustom attributes associated with the sub-account, as a map of key/value pairs. + * @param enabled Set the sub-account as enabled or not. + * @param options Generic advanced options map, see online documentation. + * @return details of the updated sub-account + * @throws Exception If the request fails + */ + public ApiResponse updateSubAccount(String subAccountId, String name, String cloudName, Map customAttributes, Boolean enabled, Map options) throws Exception { + options = verifyOptions(options); + options.put("content_type", "json"); + + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, "sub_accounts", subAccountId); + + return callAccountApi(Api.HttpMethod.PUT, uri, ObjectUtils.asMap( + "cloud_name", cloudName, + "name", name, + "custom_attributes", customAttributes, + "enabled", enabled), + options); + } + + /** + * Deletes the sub-account. + * + * @param subAccountId The id of the sub-account to delete + * @return result message. + * @throws Exception If the request fails. + */ + public ApiResponse deleteSubAccount(String subAccountId) throws Exception { + return deleteSubAccount(subAccountId, Collections.emptyMap()); + } + + /** + * Deletes the sub-account. + * + * @param subAccountId The id of the sub-account to delete + * @param options Generic advanced options map, see online documentation. + * @return result message. + * @throws Exception If the request fails. + */ + public ApiResponse deleteSubAccount(String subAccountId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, "sub_accounts", subAccountId); + return callAccountApi(Api.HttpMethod.DELETE, uri, Collections.emptyMap(), options); + } + + // Users + /** + * Get details of a specific user. + * + * @param userId The id of the user to fetch + * @return details of the user. + * @throws Exception If the request fails. + */ + public ApiResponse user(String userId) throws Exception { + return user(userId,null); + } + + /** + * Get details of a specific user. + * + * @param userId The id of the user to fetch + * @param options Generic advanced options map, see online documentation. + * @return details of the user. + * @throws Exception If the request fails. + */ + public ApiResponse user(String userId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USERS, userId); + return callAccountApi(Api.HttpMethod.GET, uri, Collections.emptyMap(), options); + } + + /** + * Get a list of the users according to filters. + * + * @param pending Optional. Limit results to pending users (true), users that are not pending (false), or all users (null) + * @param userIds Optionals. List of user IDs. Up to 100 + * @param prefix Optional. Search by prefix of the user's name or email. Case-insensitive + * @param subAccountId Optional. Return only users who have access to the given sub-account + * @return the users' details. + * @throws Exception If the request fails. + */ + public ApiResponse users(Boolean pending, List userIds, String prefix, String subAccountId) throws Exception { + return users(pending, userIds, prefix, subAccountId,null); + } + + /** + * Get a list of the users according to filters. + * + * @param pending Optional. Limit results to pending users (true), users that are not pending (false), or all users (null) + * @param userIds Optionals. List of user IDs. Up to 100 + * @param prefix Optional. Search by prefix of the user's name or email. Case-insensitive + * @param subAccountId Optional. Return only users who have access to the given sub-account + * @param options Generic advanced options map, see online documentation. + * @return the users' details. + * @throws Exception If the request fails. + */ + public ApiResponse users(Boolean pending, List userIds, String prefix, String subAccountId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USERS); + return callAccountApi(Api.HttpMethod.GET, uri, + ObjectUtils.asMap("accountId", accountId, + "pending", pending, + "ids", userIds, + "prefix", prefix, + "sub_account_id", subAccountId), options); + } + + /** + * Create a new user. + * + * @param name Required. Username. + * @param email Required. User's email. + * @param role Required. User's role. + * @param subAccountsIds Optional. Sub-accounts for which the user should have access. + * If not provided or empty, user should have access to all accounts. + * @return The newly created user details. + * @throws Exception If the request fails. + */ + public ApiResponse createUser(String name, String email, Role role, List subAccountsIds) throws Exception { + return createUser(name, email, role, subAccountsIds, null); + } + + /** + * Create a new user. + * + * @param name Required. Username. + * @param email Required. User's email. + * @param role Required. User's role. + * @param subAccountsIds Optional. Sub-accounts for which the user should have access. + * If not provided or empty, user should have access to all accounts. + * @param options Generic advanced options map, see online documentation. + * @return The newly created user details. + * @throws Exception If the request fails. + */ + public ApiResponse createUser(String name, String email, Role role, List subAccountsIds, Map options) throws Exception { + return createUser(name, email, role, null, subAccountsIds, options); + } + + /** + * Create a new user. + * + * @param name Required. Username. + * @param email Required. User's email. + * @param role Required. User's role. + * @param enabled Optional. User's status (enabled or disabled). + * @param subAccountsIds Optional. Sub-accounts for which the user should have access. + * If not provided or empty, user should have access to all accounts. + * @param options Generic advanced options map, see online documentation. + * @return The newly created user details. + * @throws Exception If the request fails. + */ + public ApiResponse createUser(String name, String email, Role role, Boolean enabled, List subAccountsIds, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USERS); + return performUserAction(Api.HttpMethod.POST, uri, email, name, role, enabled, subAccountsIds, options); + } + + /** + * Update an existing user. + * + * @param userId The id of the user to update. + * @param name Username. + * @param email User's email. + * @param role User's role. + * @param subAccountsIds Sub-accounts for which the user should have access. + * If not provided or empty, user should have access to all accounts. + * @return The updated user details + * @throws Exception If the request fails. + */ + public ApiResponse updateUser(String userId, String name, String email, Role role, List subAccountsIds) throws Exception { + return updateUser(userId, name, email, role, subAccountsIds,null); + } + + /** + * Update an existing user. + * + * @param userId The id of the user to update. + * @param name Username. + * @param email User's email. + * @param role User's role. + * @param subAccountsIds Sub-accounts for which the user should have access. + * If not provided or empty, user should have access to all accounts. + * @param options Generic advanced options map, see online documentation. + * @return The updated user details + * @throws Exception If the request fails. + */ + public ApiResponse updateUser(String userId, String name, String email, Role role, List subAccountsIds, Map options) throws Exception { + return updateUser(userId, name ,email ,role ,null , subAccountsIds , options); + } + + /** + * Update an existing user. + * + * @param userId The id of the user to update. + * @param name Username. + * @param email User's email. + * @param role User's role. + * @param enabled User's status (enabled or disabled) + * @param subAccountsIds Sub-accounts for which the user should have access. + * If not provided or empty, user should have access to all accounts. + * @param options Generic advanced options map, see online documentation. + * @return The updated user details + * @throws Exception If the request fails. + */ + public ApiResponse updateUser(String userId, String name, String email, Role role, Boolean enabled, List subAccountsIds, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USERS, userId); + return performUserAction(Api.HttpMethod.PUT, uri, email, name, role, enabled, subAccountsIds, options); + } + + /** + * Delete a user. + * + * @param userId Id of the user to delete. + * @return result message. + * @throws Exception + */ + public ApiResponse deleteUser(String userId) throws Exception { + return deleteUser(userId,null); + } + + /** + * Delete a user. + * + * @param userId Id of the user to delete. + * @param options Generic advanced options map, see online documentation. + * @return result message. + * @throws Exception + */ + public ApiResponse deleteUser(String userId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USERS, userId); + return callAccountApi(Api.HttpMethod.DELETE, uri, Collections.emptyMap(), options); + } + + // Groups + /** + * Create a new user group + * @param name Required. Name for the group. + * @return The newly created group. + * @throws Exception If the request fails + */ + public ApiResponse createUserGroup(String name) throws Exception { + return createUserGroup(name,null); + } + + /** + * Create a new user group + * @param name Required. Name for the group. + * @param options Generic advanced options map, see online documentation. + * @return The newly created group. + * @throws Exception If the request fails + */ + public ApiResponse createUserGroup(String name, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS); + return callAccountApi(Api.HttpMethod.POST, uri, ObjectUtils.asMap("name", name), options); + } + + /** + * Update an existing user group + * + * @param groupId The id of the group to update + * @param name The name of the group. + * @return The updated group. + * @throws Exception If the request fails + */ + public ApiResponse updateUserGroup(String groupId, String name) throws Exception { + return updateUserGroup(groupId, name,null); + } + + /** + * Update an existing user group + * + * @param groupId The id of the group to update + * @param name The name of the group. + * @param options Generic advanced options map, see online documentation. + * @return The updated group. + * @throws Exception If the request fails + */ + public ApiResponse updateUserGroup(String groupId, String name, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS, groupId); + return callAccountApi(Api.HttpMethod.PUT, uri, ObjectUtils.asMap("name", name), options); + } + + /** + * Delete a user group + * + * @param groupId The group id to delete + * @return A result message. + * @throws Exception if the request fails. + */ + public ApiResponse deleteUserGroup(String groupId) throws Exception { + return deleteUserGroup(groupId,null); + } + + /** + * Delete a user group + * + * @param groupId The group id to delete + * @param options Generic advanced options map, see online documentation. + * @return A result message. + * @throws Exception if the request fails. + */ + public ApiResponse deleteUserGroup(String groupId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS, groupId); + return callAccountApi(Api.HttpMethod.DELETE, uri, Collections.emptyMap(), options); + } + + /** + * Add an existing user to a group. + * @param groupId The group id. + * @param userId The user id to add. + * @throws Exception If the request fails + */ + public ApiResponse addUserToGroup(String groupId, String userId) throws Exception { + return addUserToGroup(groupId, userId,null); + } + /** + * Add an existing user to a group. + * @param groupId The group id. + * @param userId The user id to add. + * @param options Generic advanced options map, see online documentation. + * @throws Exception If the request fails + */ + public ApiResponse addUserToGroup(String groupId, String userId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS, groupId, USERS, userId); + return callAccountApi(Api.HttpMethod.POST, uri, Collections.emptyMap(), options); + } + + /** + * Removes a user from a group. + * @param groupId The group id. + * @param userId The id of the user to remove + * @return A result message + * @throws Exception If the request fails. + */ + public ApiResponse removeUserFromGroup(String groupId, String userId) throws Exception { + return removeUserFromGroup(groupId, userId,null); + } + /** + * Removes a user from a group. + * @param groupId The group id. + * @param userId The id of the user to remove + * @param options Generic advanced options map, see online documentation. + * @return A result message + * @throws Exception If the request fails. + */ + public ApiResponse removeUserFromGroup(String groupId, String userId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS, groupId, USERS, userId); + return callAccountApi(Api.HttpMethod.DELETE, uri, Collections.emptyMap(), options); + } + + /** + * Get details of a group. + * @param groupId The group id to fetch + * @return Details of the group. + * @throws Exception If the request fails. + */ + public ApiResponse userGroup(String groupId) throws Exception { + return userGroup(groupId,null); + } + + /** + * Get details of a group. + * @param groupId The group id to fetch + * @param options Generic advanced options map, see online documentation. + * @return Details of the group. + * @throws Exception If the request fails. + */ + public ApiResponse userGroup(String groupId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS, groupId); + return callAccountApi(Api.HttpMethod.GET, uri, Collections.emptyMap(), options); + } + + /** + * Gets a list of all the user groups. + * @return The list of the groups. + * @throws Exception If the request fails. + */ + public ApiResponse userGroups() throws Exception { + return userGroups(Collections.emptyMap()); + } + + /** + * Gets a list of all the user groups. + * @param options Generic advanced options map, see online documentation. + * @return The list of the groups. + * @throws Exception If the request fails. + */ + public ApiResponse userGroups(Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS); + return callAccountApi(Api.HttpMethod.GET, uri, Collections.emptyMap(), options); + } + + /** + * Lists the users belonging to this user group. + * @param groupId The id of the user group. + * @return The list of users in that group. + * @throws Exception If the request fails. + */ + public ApiResponse userGroupUsers(String groupId) throws Exception { + return userGroupUsers(groupId,null); + } + /** + * Lists the users belonging to this user group. + * @param groupId The id of the user group. + * @param options Generic advanced options map, see online documentation. + * @return The list of users in that group. + * @throws Exception If the request fails. + */ + public ApiResponse userGroupUsers(String groupId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, USER_GROUPS, groupId, USERS); + return callAccountApi(Api.HttpMethod.GET, uri, Collections.emptyMap(), options); + } + + /** + * Lists the access keys belonging to this sub account id. + * @param subAccountId The id of the user group. + * @param options Generic advanced options map, see online documentation. + * @return The list of access keys in that sub account id. + * @throws Exception If the request fails. + */ + public ApiResponse getAccessKeys(String subAccountId, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, SUB_ACCOUNTS, subAccountId); + return callAccountApi(Api.HttpMethod.GET, uri, Collections.emptyMap(), options); + } + + /** + * Creates a new access key for this sub account id. + * @param subAccountId The id of the user group. + * @param name The name for the access key. + * @param enabled Access key's status (enabled or disabled). + * @param options Generic advanced options map, see online documentation. + * @return The created access key. + * @throws Exception If the request fails. + */ + public ApiResponse createAccessKey(String subAccountId, String name, Boolean enabled, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, SUB_ACCOUNTS, subAccountId, ACCESS_KEYS); + return callAccountApi(Api.HttpMethod.POST, uri, ObjectUtils.asMap("name", name, "enabled", enabled), options); + } + + /** + * Updates an existing access key for this sub account id. + * @param subAccountId The id of the user group. + * @param accessKey The key of the access key. + * @param name The name for the access key. + * @param enabled Access key's status (enabled or disabled). + * @param options Generic advanced options map, see online documentation. + * @return The updated access key. + * @throws Exception If the request fails. + */ + public ApiResponse updateAccessKey(String subAccountId, String accessKey, String name, Boolean enabled, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, SUB_ACCOUNTS, subAccountId, ACCESS_KEYS, accessKey); + return callAccountApi(Api.HttpMethod.PUT, uri, ObjectUtils.asMap("name", name, "enabled", enabled), options); + } + + /** + * Deletes an existing access key for this sub account id. + * @param subAccountId The id of the user group. + * @param accessKey The key of the access key. + * @param options Generic advanced options map, see online documentation. + * @return "message": "ok". + * @throws Exception If the request fails. + */ + public ApiResponse deleteAccessKey(String subAccountId, String accessKey, Map options) throws Exception { + List uri = Arrays.asList(PROVISIONING, ACCOUNTS, accountId, SUB_ACCOUNTS, subAccountId, ACCESS_KEYS, accessKey); + return callAccountApi(Api.HttpMethod.DELETE, uri, Collections.emptyMap(), options); + } + + /** + * Private helper method for users api calls + * @param method Http method + * @param uri Uri to call + * @param email user email + * @param name user name + * @param role user role + * @param subAccountsIds suv accounts ids the user has access to. + * @param options + * @return The response of the api call. + * @throws Exception If the request fails. + */ + private ApiResponse performUserAction(Api.HttpMethod method, List uri, String email, String name, Role role, Boolean enabled, List subAccountsIds, Map options) throws Exception { + options = verifyOptions(options); + options.put("content_type", "json"); + + return callAccountApi(method, uri, ObjectUtils.asMap( + "email", email, + "name", name, + "role", role == null ? null : role.serializedValue, + "enabled", enabled, + "sub_account_ids", subAccountsIds), + options); + } + + private Map verifyOptions(Map options) { + if (options == null || options == Collections.EMPTY_MAP) { + return new HashMap(2); // Two, since api key and secret will be populated later + } + + return options; + } + + protected String getAuthorizationHeaderValue(String apiKey, String apiSecret, String oauthToken) { + if (oauthToken != null){ + return "Bearer " + oauthToken; + } else { + return "Basic " + Base64Coder.encodeString(apiKey + ":" + apiSecret); + } + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/provisioning/AccountConfiguration.java b/cloudinary-core/src/main/java/com/cloudinary/provisioning/AccountConfiguration.java new file mode 100644 index 00000000..2d52ca43 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/provisioning/AccountConfiguration.java @@ -0,0 +1,35 @@ +package com.cloudinary.provisioning; + +import com.cloudinary.utils.StringUtils; + +import java.net.URI; + +public class AccountConfiguration { + private static final String SEPARATOR = ":"; + String accountId; + String provisioningApiKey; + String provisioningApiSecret; + + public AccountConfiguration(String accountId, String provisioningApiKey, String provisioningApiSecret) { + this.accountId = accountId; + this.provisioningApiKey = provisioningApiKey; + this.provisioningApiSecret = provisioningApiSecret; + } + + public static AccountConfiguration from(String accountUrl) { + URI uri = URI.create(accountUrl); + + String accountId = uri.getHost(); + if (StringUtils.isBlank(accountId)) throw new IllegalArgumentException("Account id must be provided in account url"); + + if (uri.getUserInfo() == null) throw new IllegalArgumentException("Full credentials (key+secret) must be provided in account url"); + String[] credentials = uri.getUserInfo().split(":"); + if (credentials.length < 2 || + StringUtils.isBlank(credentials[0]) || + StringUtils.isBlank(credentials[1])) { + throw new IllegalArgumentException("Full credentials (key+secret) must be provided in account url"); + } + + return new AccountConfiguration(accountId, credentials[0], credentials[1]); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java b/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java new file mode 100644 index 00000000..0342f5bc --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractApiStrategy.java @@ -0,0 +1,20 @@ +package com.cloudinary.strategies; + +import com.cloudinary.Api; +import com.cloudinary.Api.HttpMethod; +import com.cloudinary.api.ApiResponse; +import java.util.Map; + + +public abstract class AbstractApiStrategy { + protected Api api; + + public void init(Api api) { + this.api = api; + } + + @SuppressWarnings("rawtypes") + public abstract ApiResponse callApi(HttpMethod method, String apiUrl, Map params, Map options, String authorizationHeader) throws Exception; + + public abstract ApiResponse callAccountApi(HttpMethod method, String apiUrl, Map params, Map options, String authorizationHeader) throws Exception; +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractUploaderStrategy.java b/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractUploaderStrategy.java new file mode 100644 index 00000000..d259e099 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/strategies/AbstractUploaderStrategy.java @@ -0,0 +1,102 @@ +package com.cloudinary.strategies; + +import com.cloudinary.Cloudinary; +import com.cloudinary.ProgressCallback; +import com.cloudinary.Uploader; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.cloudinary.json.JSONException; +import org.cloudinary.json.JSONObject; + +import java.io.IOException; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +public abstract class AbstractUploaderStrategy { + private final static int[] ERROR_CODES = new int[]{400, 401, 403, 404, 420, 500}; + protected Uploader uploader; + + public void init(Uploader uploader) { + this.uploader = uploader; + } + + public Cloudinary cloudinary() { + return this.uploader.cloudinary(); + } + + @SuppressWarnings("rawtypes") + public Map callApi(String action, Map params, Map options, Object file) throws IOException { + return callApi(action, params, options, file, null); + } + + public abstract Map callApi(String action, Map params, Map options, Object file, ProgressCallback progressCallback) throws IOException; + + protected String buildUploadUrl(String action, Map options) { + String cloudinary = ObjectUtils.asString(options.get("upload_prefix"), + ObjectUtils.asString(uploader.cloudinary().config.uploadPrefix, "https://api.cloudinary.com")); + String cloud_name = ObjectUtils.asString(options.get("cloud_name"), ObjectUtils.asString(uploader.cloudinary().config.cloudName)); + if (cloud_name == null) + throw new IllegalArgumentException("Must supply cloud_name in tag or in configuration"); + + if (action.equals("delete_by_token")) { + // delete_by_token doesn't need resource_type + return StringUtils.join(new String[]{cloudinary, "v1_1", cloud_name, action}, "/"); + } else { + String resource_type = ObjectUtils.asString(options.get("resource_type"), "image"); + return StringUtils.join(new String[]{cloudinary, "v1_1", cloud_name, resource_type, action}, "/"); + } + } + + protected Map processResponse(boolean returnError, int code, String responseData) { + String errorMessage = null; + Map result = null; + if (code == 200 || includesServerResponse(code)) { + try { + JSONObject responseJSON = new JSONObject(responseData); + result = ObjectUtils.toMap(responseJSON); + + if (result.containsKey("error")) { + Map error = (Map) result.get("error"); + error.put("http_code", code); + errorMessage = (String) error.get("message"); + } + } catch (JSONException e) { + errorMessage = "Invalid JSON response from server " + e.getMessage(); + } + } else { + errorMessage = "Server returned unexpected status code - " + code; + if (StringUtils.isNotBlank(responseData)) { + errorMessage += (" - " + responseData); + } + } + + if (StringUtils.isNotBlank(errorMessage)) { + if (returnError) { + // return a result containing the error instead of throwing an exception: + if (result == null) { + Map error = new HashMap(); + error.put("http_code", code); + error.put("message", errorMessage); + result = new HashMap(); + result.put("error", error); + } // else - Result is already built, with the error inside. Nothing to do. + } else { + throw new RuntimeException(errorMessage); + } + } + + return result; + } + + private boolean includesServerResponse(int code) { + return Arrays.binarySearch(ERROR_CODES, code) >= 0; + } + + protected boolean requiresSigning(String action, Map options) { + boolean unsigned = Boolean.TRUE.equals(options.get("unsigned")); + boolean deleteByToken = "delete_by_token".equals(action); + + return !unsigned && !deleteByToken; + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/strategies/StrategyLoader.java b/cloudinary-core/src/main/java/com/cloudinary/strategies/StrategyLoader.java new file mode 100644 index 00000000..66d37b83 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/strategies/StrategyLoader.java @@ -0,0 +1,33 @@ +package com.cloudinary.strategies; + +import java.util.List; + +public class StrategyLoader { + + @SuppressWarnings("unchecked") + public static T load(String className) { + T result = null; + try { + Class clazz = Class.forName(className); + result = (T) clazz.newInstance(); + } catch (Exception e) { + } + return result; + } + + public static T find(List strategies) { + for (int i = 0; i < strategies.size(); i++) { + T strategy = load(strategies.get(i)); + if (strategy != null) { + return strategy; + } + } + return null; + + } + + public boolean exists(List strategies) { + return find(strategies) != null; + } + +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/AbstractLayer.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/AbstractLayer.java new file mode 100644 index 00000000..513b10d7 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/AbstractLayer.java @@ -0,0 +1,66 @@ +package com.cloudinary.transformation; + +import java.io.Serializable; +import java.util.ArrayList; + +import com.cloudinary.utils.StringUtils; + +public abstract class AbstractLayer> implements Serializable{ + abstract T getThis(); + + protected String resourceType = null; + protected String type = null; + protected String publicId = null; + protected String format = null; + + public T resourceType(String resourceType) { + this.resourceType = resourceType; + return getThis(); + } + + public T type(String type) { + this.type = type; + return getThis(); + } + + public T publicId(String publicId) { + this.publicId = publicId.replace('/', ':'); + return getThis(); + } + + public T format(String format) { + this.format = format; + return getThis(); + } + + @Override + public String toString() { + ArrayList components = new ArrayList(); + + if (this.resourceType != null && !this.resourceType.equals("image")) { + components.add(this.resourceType); + } + + if (this.type != null && !this.type.equals("upload")) { + components.add(this.type); + } + + if (this.publicId == null) { + throw new IllegalArgumentException("Must supply publicId"); + } + + components.add(formattedPublicId()); + + return StringUtils.join(components, ":"); + } + + protected String formattedPublicId() { + String transientPublicId = this.publicId; + + if (this.format != null) { + transientPublicId = transientPublicId + "." + this.format; + } + + return transientPublicId; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/BaseExpression.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/BaseExpression.java new file mode 100644 index 00000000..a4ed118c --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/BaseExpression.java @@ -0,0 +1,292 @@ +package com.cloudinary.transformation; + +import com.cloudinary.Transformation; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Defines an expression used in transformation parameter values + * + * @param Children must define themselves as T + */ +public abstract class BaseExpression { + public static final Map OPERATORS = ObjectUtils.asMap( + "=", "eq", + "!=", "ne", + "<", "lt", + ">", "gt", + "<=", "lte", + ">=", "gte", + "&&", "and", + "||", "or", + "*", "mul", + "/", "div", + "+", "add", + "-", "sub", + "^", "pow" + ); + public static final Map PREDEFINED_VARS = ObjectUtils.asMap( + "width", "w", + "height", "h", + "initialWidth", "iw", + "initialHeight", "ih", + "aspect_ratio", "ar", + "initial_aspect_ratio", "iar", + "aspectRatio", "ar", + "initialAspectRatio", "iar", + "page_count", "pc", + "pageCount", "pc", + "face_count", "fc", + "faceCount", "fc", + "current_page", "cp", + "currentPage", "cp", + "tags", "tags", + "pageX", "px", + "pageY", "py", + "duration","du", + "initial_duration","idu", + "initialDuration","idu" + + ); + private static final Pattern PATTERN = getPattern(); + private static final Pattern USER_VARIABLE_PATTERN = Pattern.compile("\\$_*[^_]+"); + + protected List expressions = null; + protected Transformation parent = null; + + protected BaseExpression() { + expressions = new ArrayList(); + } + + /** + * Normalize an expression string, replace "nice names" with their coded values and spaces with "_". + * + * @param expression an expression + * @return a parsed expression + */ + public static String normalize(Object expression) { + if (expression == null) { + return null; + } + + // If it's a number it's not an expression + if (expression instanceof Number){ + return String.valueOf(expression); + } + + String conditionStr = StringUtils.mergeToSingleUnderscore(String.valueOf(expression)); + + Matcher m = USER_VARIABLE_PATTERN.matcher(conditionStr); + StringBuilder builder = new StringBuilder(); + int lastMatchEnd = 0; + while (m.find()) { + String beforeMatch = conditionStr.substring(lastMatchEnd, m.start()); + builder.append(normalizeBuiltins(beforeMatch)); + builder.append(m.group()); + lastMatchEnd = m.end(); + } + builder.append(normalizeBuiltins(conditionStr.substring(lastMatchEnd))); + return builder.toString(); + } + + private static String normalizeBuiltins(String input) { + String replacement; + Matcher matcher = PATTERN.matcher(input); + StringBuffer result = new StringBuffer(input.length()); + while (matcher.find()) { + if (OPERATORS.containsKey(matcher.group())) { + replacement = (String) OPERATORS.get(matcher.group()); + } else if (PREDEFINED_VARS.containsKey(matcher.group())) { + replacement = (String) PREDEFINED_VARS.get(matcher.group()); + } else { + replacement = matcher.group(); + } + matcher.appendReplacement(result, replacement); + } + matcher.appendTail(result); + return result.toString(); + } + + /** + * @return a regex pattern for operators and predefined vars as /((operators)(?=[ _])|variables)/ + */ + private static Pattern getPattern() { + String pattern; + final ArrayList operators = new ArrayList(OPERATORS.keySet()); + Collections.sort(operators, Collections.reverseOrder()); + StringBuilder sb = new StringBuilder("(("); + for (String op : operators) { + sb.append(Pattern.quote(op)).append("|"); + } + sb.deleteCharAt(sb.length() - 1); + sb.append(")(?=[ _])|(? { + + public Condition() { + super(); + + } + + /** + * Create a Condition Object. The conditionStr string will be translated to a serialized condition. + *
+ * For example, new Condition("fc > 3") + * @param conditionStr condition in string format + */ + public Condition(String conditionStr) { + this(); + if (conditionStr != null) { + expressions.add(normalize(conditionStr)); + } + } + + @Override + protected Condition newInstance() { + return new Condition(); + } + + protected Condition predicate(String name, String operator, Object value) { + if (OPERATORS.containsKey(operator)) { + operator = (String) OPERATORS.get(operator); + } + expressions.add(String.format("%s_%s_%s", name, operator, value)); + return this; + } + + /** + * Terminates the definition of the condition and continue with Transformation definition. + * @return the Transformation object this Condition is attached to. + */ + public Transformation then() { + getParent().ifCondition(serialize()); + return getParent(); + } + + public Condition width(String operator, Object value) { + return predicate("w", operator, value); + } + + public Condition height(String operator, Object value) { + return predicate("h", operator, value); + } + + public Condition aspectRatio(String operator, Object value) { + return predicate("ar", operator, value); + } + public Condition duration(String operator, Object value) { + return predicate("du", operator, value); + } + public Condition initialDuration(String operator, Object value) { + return predicate("idu", operator, value); + } + + public Condition faceCount(String operator, Object value) { + return predicate("fc", operator, value); + } + + public Condition pageCount(String operator, Object value) { + return predicate("pc", operator, value); + } + +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/Expression.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/Expression.java new file mode 100644 index 00000000..d8e2558f --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/Expression.java @@ -0,0 +1,99 @@ +package com.cloudinary.transformation; + +/** + * Represents a transformation parameter expression. + */ +public class Expression extends BaseExpression { + + private boolean predefined = false; + + public Expression(){ + super(); + } + + public Expression(String name){ + super(); + expressions.add(name); + } + + public static Expression variable(String name, Object value){ + Expression var = new Expression(name); + var.expressions.add(value.toString()); + return var; + } + + public static Expression faceCount() { + return new Expression("fc"); + } + + @Override + protected Expression newInstance() { + return new Expression(); + } + /* + * @returns a new expression with the predefined variable "width" + */ + public static Expression width() { + return new Expression("width"); + } + /* + * @returns a new expression with the predefined variable "height" + */ + public static Expression height() { + return new Expression("height"); + } + /* + * @returns a new expression with the predefined variable "initialWidth" + */ + public static Expression initialWidth() { + return new Expression("initialWidth"); + } + /* + * @returns a new expression with the predefined variable "initialHeight" + */ + public static Expression initialHeight() { + return new Expression("initialHeight"); + } + /* + * @returns a new expression with the predefined variable "aspectRatio" + */ + public static Expression aspectRatio() { + return new Expression("aspectRatio"); + } + /* + * @returns a new expression with the predefined variable "initialAspectRatio" + */ + public static Expression initialAspectRatio() { + return new Expression("initialAspectRatio"); + } + /* + * @returns a new expression with the predefined variable "pageCount" + */ + public static Expression pageCount() { + return new Expression("pageCount"); + } + /* + * @returns a new expression with the predefined variable "currentPage" + */ + public static Expression currentPage() { + return new Expression("currentPage"); + } + /* + * @returns a new expression with the predefined variable "tags" + */ + public static Expression tags() { + return new Expression("tags"); + } + /* + * @returns a new expression with the predefined variable "pageX" + */ + public static Expression pageX() { + return new Expression("pageX"); + } + /* + * @returns a new expression with the predefined variable "pageY" + */ + public static Expression pageY() { + return new Expression("pageY"); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/FetchLayer.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/FetchLayer.java new file mode 100644 index 00000000..011a88e9 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/FetchLayer.java @@ -0,0 +1,25 @@ +package com.cloudinary.transformation; + +import com.cloudinary.utils.Base64Coder; + +public class FetchLayer extends AbstractLayer { + + public FetchLayer() { + this.type = "fetch"; + } + + public FetchLayer url(String remoteUrl) { + this.publicId = Base64Coder.encodeURLSafeString(remoteUrl);; + return this; + } + + @Override + public FetchLayer type(String type) { + throw new UnsupportedOperationException("Cannot modify type for fetch layers"); + } + + @Override + FetchLayer getThis() { + return this; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/Layer.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/Layer.java new file mode 100644 index 00000000..1cdad76e --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/Layer.java @@ -0,0 +1,8 @@ +package com.cloudinary.transformation; + +public class Layer extends AbstractLayer { + @Override + Layer getThis() { + return this; + } +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/SubtitlesLayer.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/SubtitlesLayer.java new file mode 100644 index 00000000..006bc547 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/SubtitlesLayer.java @@ -0,0 +1,7 @@ +package com.cloudinary.transformation; + +public class SubtitlesLayer extends TextLayer { + public SubtitlesLayer() { + this.resourceType = "subtitles"; + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/transformation/TextLayer.java b/cloudinary-core/src/main/java/com/cloudinary/transformation/TextLayer.java new file mode 100644 index 00000000..55380df3 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/transformation/TextLayer.java @@ -0,0 +1,213 @@ +package com.cloudinary.transformation; + +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.cloudinary.SmartUrlEncoder; +import com.cloudinary.utils.StringUtils; + +public class TextLayer extends AbstractLayer { + protected String resourceType = "text"; + protected String fontFamily = null; + protected Integer fontSize = null; + protected String fontWeight = null; + protected String fontStyle = null; + protected String fontAntialiasing = null; + protected String fontHinting=null; + protected String textDecoration = null; + protected String textAlign = null; + protected String stroke = null; + protected String letterSpacing = null; + protected Integer lineSpacing = null; + protected String text = null; + protected Object textStyle = null; + + @Override + TextLayer getThis() { + return this; + } + + public TextLayer resourceType(String resourceType) { + throw new UnsupportedOperationException("Cannot modify resourceType for text layers"); + } + + public TextLayer type(String type) { + throw new UnsupportedOperationException("Cannot modify type for text layers"); + } + + public TextLayer format(String format) { + throw new UnsupportedOperationException("Cannot modify format for text layers"); + } + + public TextLayer fontFamily(String fontFamily) { + this.fontFamily = fontFamily; + return getThis(); + } + + public TextLayer fontAntialiasing(String fontAntialiasing) { + this.fontAntialiasing = fontAntialiasing; + return getThis(); + } + + public TextLayer fontHinting(String fontHinting) { + this.fontHinting = fontHinting; + return getThis(); + } + + + public TextLayer fontSize(int fontSize) { + this.fontSize = fontSize; + return getThis(); + } + + public TextLayer fontWeight(String fontWeight) { + this.fontWeight = fontWeight; + return getThis(); + } + + public TextLayer fontStyle(String fontStyle) { + this.fontStyle = fontStyle; + return getThis(); + } + + public TextLayer textDecoration(String textDecoration) { + this.textDecoration = textDecoration; + return getThis(); + } + + public TextLayer textAlign(String textAlign) { + this.textAlign = textAlign; + return getThis(); + } + + public TextLayer stroke(String stroke) { + this.stroke = stroke; + return getThis(); + } + + public TextLayer letterSpacing(String letterSpacing) { + this.letterSpacing = letterSpacing; + return getThis(); + } + + public TextLayer letterSpacing(int letterSpacing) { + this.letterSpacing = String.valueOf(letterSpacing); + return getThis(); + } + + public TextLayer lineSpacing(Integer lineSpacing) { + this.lineSpacing = lineSpacing; + return getThis(); + } + + public TextLayer text(String text) { + String part; + StringBuffer result = new StringBuffer(); + // Don't encode interpolation expressions e.g. $(variable) + Matcher m = Pattern.compile("\\$\\([a-zA-Z]\\w+\\)").matcher(text); + int start = 0; + while (m.find()) { + part = text.substring(start, m.start()); + part = SmartUrlEncoder.encode(part); + result.append(part); // append encoded pre-match + result.append(m.group()); // append match + start = m.end(); + } + result.append(SmartUrlEncoder.encode(text.substring(start))); + this.text = result.toString().replace("%2C", "%252C").replace("/", "%252F"); + return getThis(); + } + + /** + * Sets a text style identifier, + * Note: If this is used, all other style attributes are ignored in favor of this identifier + * @param textStyleIdentifier A variable string or an explicit style (e.g. "Arial_17_bold_antialias_best") + * @return Itself for chaining + */ + public TextLayer textStyle(String textStyleIdentifier) { + this.textStyle = textStyleIdentifier; + return getThis(); + } + + /** + * Sets a text style identifier using an expression. + * Note: If this is used, all other style attributes are ignored in favor of this identifier + * @param textStyleIdentifier An expression instance referencing the style. + * @return Itself for chaining + */ + public TextLayer textStyle(Expression textStyleIdentifier) { + this.textStyle = textStyleIdentifier; + return getThis(); + } + + @Override + public String toString() { + if (this.publicId == null && this.text == null) { + throw new IllegalArgumentException("Must supply either text or public_id."); + } + + ArrayList components = new ArrayList(); + components.add(this.resourceType); + + String styleIdentifier = textStyleIdentifier(); + if (styleIdentifier != null) { + components.add(styleIdentifier); + } + + if (this.publicId != null) { + components.add(this.formattedPublicId()); + } + + if (this.text != null) { + components.add(this.text); + } + + return StringUtils.join(components, ":"); + } + + protected String textStyleIdentifier() { + // Note: if a text-style argument is provided as a whole, it overrides everything else, no mix and match. + if (StringUtils.isNotBlank(this.textStyle)) { + return textStyle.toString(); + } + + ArrayList components = new ArrayList(); + + if (StringUtils.isNotBlank(this.fontWeight) && !this.fontWeight.equals("normal")) + components.add(this.fontWeight); + if (StringUtils.isNotBlank(this.fontStyle) && !this.fontStyle.equals("normal")) + components.add(this.fontStyle); + if (StringUtils.isNotBlank(this.fontAntialiasing)) + components.add("antialias_"+this.fontAntialiasing); + if (StringUtils.isNotBlank(this.fontHinting)) + components.add("hinting_"+this.fontHinting); + if (StringUtils.isNotBlank(this.textDecoration) && !this.textDecoration.equals("none")) + components.add(this.textDecoration); + if (StringUtils.isNotBlank(this.textAlign)) + components.add(this.textAlign); + if (StringUtils.isNotBlank(this.stroke) && !this.stroke.equals("none")) + components.add(this.stroke); + if (StringUtils.isNotBlank(this.letterSpacing)) + components.add("letter_spacing_" + this.letterSpacing); + if (this.lineSpacing != null) + components.add("line_spacing_" + this.lineSpacing.toString()); + + if (this.fontFamily == null && this.fontSize == null && components.isEmpty()) { + return null; + } + + if (this.fontFamily == null) { + throw new IllegalArgumentException("Must supply fontFamily."); + } + + if (this.fontSize == null) { + throw new IllegalArgumentException("Must supply fontSize."); + } + + components.add(0, Integer.toString(this.fontSize)); + components.add(0, this.fontFamily); + + return StringUtils.join(components, "_"); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/Analytics.java b/cloudinary-core/src/main/java/com/cloudinary/utils/Analytics.java new file mode 100644 index 00000000..55001eb2 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/Analytics.java @@ -0,0 +1,158 @@ +package com.cloudinary.utils; + +import com.cloudinary.Cloudinary; + +import java.util.Arrays; +import java.util.List; + +public class Analytics { + private String sdkTokenQueryKey = "_a"; //sdkTokenQueryKey + private String sdkQueryDelimiter = "="; + public String algoVersion = "D"; + public String prodcut = "A"; + public String SDKCode = ""; // Java = G, Android = F + public String SDKSemver = ""; // Calculate the SDK version . + public String techVersion = ""; // Calculate the Java version. + public String osType; + public String osVersion; + + public String featureFlag = "0"; + + public Analytics() { + this("G", Cloudinary.VERSION,System.getProperty("java.version"), "Z", "0.0", "0"); + } + public Analytics(String sdkCode, String sdkVersion, String techVersion, String osType, String osVersion, String featureFlag) { + this.SDKCode = sdkCode; + this.SDKSemver = sdkVersion; + this.techVersion = techVersion; + this.osType = osType; + this.osVersion = osVersion; + this.featureFlag = featureFlag; + } + + public Analytics setSDKCode(String SDKCode) { + this.SDKCode = SDKCode; + return this; + } + + public Analytics setSDKSemver(String SDKSemver) { + this.SDKSemver = SDKSemver; + return this; + } + + public Analytics setTechVersion(String techVersion) { + this.techVersion = techVersion; + return this; + } + + public Analytics setFeatureFlag(String flag) { + this.featureFlag = flag; + return this; + } + + /** + * Function turn analytics variables into viable query parameter. + * @return query param with analytics values. + */ + public String toQueryParam() { + try { + return sdkTokenQueryKey + sdkQueryDelimiter + getAlgorithmVersion() + prodcut + getSDKType() + getSDKVersion() + getTechVersion() + getOsType() + getOsVersion() + getSDKFeatureFlag(); + } catch (Exception e) { + return sdkTokenQueryKey + sdkQueryDelimiter + "E"; + } + } + + private String getTechVersion() throws Exception { + String[] techVersionString = techVersion.split("_"); + String[] versions = techVersionString[0].split("\\."); + return versionArrayToString(versions); + } + + private String versionArrayToString(String[] versions) throws Exception { + if (versions.length > 2) { + versions = Arrays.copyOf(versions, versions.length - 1); + } + return getPaddedString(StringUtils.join(versions, ".")); + } + + private String versionArrayToOsString(String[] versions) throws Exception { + if (versions.length > 2) { + versions = Arrays.copyOf(versions, versions.length - 1); + } + return getOsVersionString(StringUtils.join(versions, ".")); + } + + private String getOsType() { + return (osType != null) ? osType : "Z"; //System.getProperty("os.name"); + } + + private String getOsVersion() throws Exception { + return (osVersion != null) ? versionArrayToOsString(osVersion.split("\\.")) : versionArrayToString(System.getProperty("os.version").split("\\.")); + } + + private String getSDKType() { + return SDKCode; + } + + private String getAlgorithmVersion() { + return algoVersion; + } + + private String getSDKFeatureFlag() { + return featureFlag; + } + + private String getSDKVersion() throws Exception { + return getPaddedString(SDKSemver); + } + + private String getOsVersionString(String string) throws Exception { + String[] parts = string.split("\\."); + String result = ""; + for(int i = 0 ; i < parts.length ; i++) { + int num = Integer.parseInt(parts[i]); + String binaryString = Integer.toBinaryString(num); + binaryString = StringUtils.padStart(binaryString, 6, '0'); + result = result + Base64Map.values.get(binaryString); + } + return result; + } + + private String getPaddedString(String string) throws Exception { + String paddedReversedSemver = ""; + int parts = string.split("\\.").length; + int paddedStringLength = parts * 6; + try { + paddedReversedSemver = reverseVersion(string); + } catch (Exception e) { + throw new Exception("Error"); + } + int num = Integer.parseInt(StringUtils.join(paddedReversedSemver.split("\\."),"")); + + String paddedBinary = StringUtils.padStart(Integer.toBinaryString(num), paddedStringLength, '0'); + + if (paddedBinary.length() % 6 != 0) { + throw new Exception("Error"); + } + + String result = ""; + List resultList = StringUtils.getAllSubStringWithSize(paddedBinary,6); + int i = 0; + while (i < resultList.size()) { + result = result + Base64Map.values.get(resultList.get(i)); + i++; + } + return result; + } + + private String reverseVersion(String SDKSemver) throws Exception { + if (SDKSemver.split("\\.").length < 2) { + throw new Exception("invalid semVer, must have at least two segments"); + } + String[] versionArray = SDKSemver.split("\\."); + for (int i = 0 ; i < versionArray.length; i ++) { + versionArray[i] = StringUtils.padStart(versionArray[i], 2, '0'); + } + return StringUtils.join(StringUtils.reverseStringArray(versionArray), "."); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/Base64Coder.java b/cloudinary-core/src/main/java/com/cloudinary/utils/Base64Coder.java new file mode 100644 index 00000000..aed228ab --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/Base64Coder.java @@ -0,0 +1,299 @@ +//Copyright 2003-2010 Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland +//www.source-code.biz, www.inventec.ch/chdh +// +//This module is multi-licensed and may be used under the terms +//of any of the following licenses: +// +//EPL, Eclipse Public License, V1.0 or later, http://www.eclipse.org/legal +//LGPL, GNU Lesser General Public License, V2.1 or later, http://www.gnu.org/licenses/lgpl.html +//GPL, GNU General Public License, V2 or later, http://www.gnu.org/licenses/gpl.html +//AGPL, GNU Affero General Public License V3 or later, http://www.gnu.org/licenses/agpl.html +//AL, Apache License, V2.0 or later, http://www.apache.org/licenses +//BSD, BSD License, http://www.opensource.org/licenses/bsd-license.php +//MIT, MIT License, http://www.opensource.org/licenses/MIT +// +//Please contact the author if you need another license. +//This module is provided "as is", without warranties of any kind. +package com.cloudinary.utils; + +/** + * A Base64 encoder/decoder. + *

+ *

+ * This class is used to encode and decode data in Base64 format as described in + * RFC 1521. + * + * @author Christian d'Heureuse, Inventec Informatik AG, Zurich, Switzerland, + * www.source-code.biz + */ +public class Base64Coder { + + // The line separator string of the operating system. + private static final String systemLineSeparator = System + .getProperty("line.separator"); + + // Mapping table from 6-bit nibbles to Base64 characters. + private static final char[] map1 = new char[64]; + + static { + int i = 0; + for (char c = 'A'; c <= 'Z'; c++) + map1[i++] = c; + for (char c = 'a'; c <= 'z'; c++) + map1[i++] = c; + for (char c = '0'; c <= '9'; c++) + map1[i++] = c; + map1[i++] = '+'; + map1[i++] = '/'; + } + + // Mapping table from Base64 characters to 6-bit nibbles. + private static final byte[] map2 = new byte[128]; + + static { + for (int i = 0; i < map2.length; i++) + map2[i] = -1; + for (int i = 0; i < 64; i++) + map2[map1[i]] = (byte) i; + } + + /** + * Encodes a string into Base64 format. No blanks or line breaks are + * inserted. + * + * @param s A String to be encoded. + * @return A String containing the Base64 encoded data. + */ + public static String encodeString(String s) { + return new String(encode(s.getBytes())); + } + + /** + * Encodes a byte array into Base 64 format and breaks the output into lines + * of 76 characters. This method is compatible with + * {@code sun.misc.BASE64Encoder.encodeBuffer(byte[])}. + * + * @param in An array containing the data bytes to be encoded. + * @return A String containing the Base64 encoded data, broken into lines. + */ + public static String encodeLines(byte[] in) { + return encodeLines(in, 0, in.length, 76, systemLineSeparator); + } + + /** + * Encodes a byte array into Base 64 format and breaks the output into + * lines. + * + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in {@code in} to be processed. + * @param iLen Number of bytes to be processed in {@code in}, starting + * at {@code iOff}. + * @param lineLen Line length for the output data. Should be a multiple of 4. + * @param lineSeparator The line separator to be used to separate the output lines. + * @return A String containing the Base64 encoded data, broken into lines. + */ + public static String encodeLines( + byte[] in, int iOff, int iLen, + int lineLen, String lineSeparator) { + int blockLen = (lineLen * 3) / 4; + if (blockLen <= 0) + throw new IllegalArgumentException(); + int lines = (iLen + blockLen - 1) / blockLen; + int bufLen = ((iLen + 2) / 3) * 4 + lines * lineSeparator.length(); + StringBuilder buf = new StringBuilder(bufLen); + int ip = 0; + while (ip < iLen) { + int l = Math.min(iLen - ip, blockLen); + buf.append(encode(in, iOff + ip, l)); + buf.append(lineSeparator); + ip += l; + } + return buf.toString(); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are + * inserted in the output. + * + * @param in An array containing the data bytes to be encoded. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in) { + return encode(in, 0, in.length); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are + * inserted in the output. + * + * @param in An array containing the data bytes to be encoded. + * @param iLen Number of bytes to process in {@code in}. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in, int iLen) { + return encode(in, 0, iLen); + } + + /** + * Encodes a byte array into Base64 format. No blanks or line breaks are + * inserted in the output. + * + * @param in An array containing the data bytes to be encoded. + * @param iOff Offset of the first byte in {@code in} to be processed. + * @param iLen Number of bytes to process in {@code in}, starting at + * {@code iOff}. + * @return A character array containing the Base64 encoded data. + */ + public static char[] encode(byte[] in, int iOff, int iLen) { + int oDataLen = (iLen * 4 + 2) / 3; // output length without padding + int oLen = ((iLen + 2) / 3) * 4; // output length including padding + char[] out = new char[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++] & 0xff; + int i1 = ip < iEnd ? in[ip++] & 0xff : 0; + int i2 = ip < iEnd ? in[ip++] & 0xff : 0; + int o0 = i0 >>> 2; + int o1 = ((i0 & 3) << 4) | (i1 >>> 4); + int o2 = ((i1 & 0xf) << 2) | (i2 >>> 6); + int o3 = i2 & 0x3F; + out[op++] = map1[o0]; + out[op++] = map1[o1]; + out[op] = op < oDataLen ? map1[o2] : '='; + op++; + out[op] = op < oDataLen ? map1[o3] : '='; + op++; + } + return out; + } + + /** + * Decodes a string from Base64 format. No blanks or line breaks are allowed + * within the Base64 encoded input data. + * + * @param s A Base64 String to be decoded. + * @return A String containing the decoded data. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static String decodeString(String s) { + return new String(decode(s)); + } + + /** + * Decodes a byte array from Base64 format and ignores line separators, tabs + * and blanks. CR, LF, Tab and Space characters are ignored in the input + * data. This method is compatible with + * {@code sun.misc.BASE64Decoder.decodeBuffer(String)}. + * + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decodeLines(String s) { + char[] buf = new char[s.length()]; + int p = 0; + for (int ip = 0; ip < s.length(); ip++) { + char c = s.charAt(ip); + if (c != ' ' && c != '\r' && c != '\n' && c != '\t') + buf[p++] = c; + } + return decode(buf, 0, p); + } + + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are + * allowed within the Base64 encoded input data. + * + * @param s A Base64 String to be decoded. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(String s) { + return decode(s.toCharArray()); + } + + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are + * allowed within the Base64 encoded input data. + * + * @param in A character array containing the Base64 encoded data. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(char[] in) { + return decode(in, 0, in.length); + } + + /** + * Decodes a byte array from Base64 format. No blanks or line breaks are + * allowed within the Base64 encoded input data. + * + * @param in A character array containing the Base64 encoded data. + * @param iOff Offset of the first character in {@code in} to be + * processed. + * @param iLen Number of characters to process in {@code in}, starting + * at {@code iOff}. + * @return An array containing the decoded data bytes. + * @throws IllegalArgumentException If the input is not valid Base64 encoded data. + */ + public static byte[] decode(char[] in, int iOff, int iLen) { + if (iLen % 4 != 0) + throw new IllegalArgumentException( + "Length of Base64 encoded input string is not a multiple of 4."); + while (iLen > 0 && in[iOff + iLen - 1] == '=') + iLen--; + int oLen = (iLen * 3) / 4; + byte[] out = new byte[oLen]; + int ip = iOff; + int iEnd = iOff + iLen; + int op = 0; + while (ip < iEnd) { + int i0 = in[ip++]; + int i1 = in[ip++]; + int i2 = ip < iEnd ? in[ip++] : 'A'; + int i3 = ip < iEnd ? in[ip++] : 'A'; + if (i0 > 127 || i1 > 127 || i2 > 127 || i3 > 127) + throw new IllegalArgumentException( + "Illegal character in Base64 encoded data."); + int b0 = map2[i0]; + int b1 = map2[i1]; + int b2 = map2[i2]; + int b3 = map2[i3]; + if (b0 < 0 || b1 < 0 || b2 < 0 || b3 < 0) + throw new IllegalArgumentException( + "Illegal character in Base64 encoded data."); + int o0 = (b0 << 2) | (b1 >>> 4); + int o1 = ((b1 & 0xf) << 4) | (b2 >>> 2); + int o2 = ((b2 & 3) << 6) | b3; + out[op++] = (byte) o0; + if (op < oLen) + out[op++] = (byte) o1; + if (op < oLen) + out[op++] = (byte) o2; + } + return out; + } + + // Dummy constructor. + private Base64Coder() { + } + + public static String encodeURLSafeString(String s) { + return encodeURLSafeString(s.getBytes()); + } + + public static String encodeURLSafeString(byte[] digest) { + char[] encode = encode(digest); + for (int i = 0; i < encode.length; i++) { + if (encode[i] == '+') { + encode[i] = '-'; + } else if (encode[i] == '/') { + encode[i] = '_'; + } + } + return new String(encode); + } + +} // end class Base64Coder \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/Base64Map.java b/cloudinary-core/src/main/java/com/cloudinary/utils/Base64Map.java new file mode 100644 index 00000000..f9948974 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/Base64Map.java @@ -0,0 +1,78 @@ +package com.cloudinary.utils; + +import java.util.HashMap; +import java.util.Map; + +public final class Base64Map { + private Base64Map() {} + + public static Map values; + + static { + values = new HashMap<>(); + values.put("000000", "A"); + values.put("000001", "B"); + values.put("000010", "C"); + values.put("000011", "D"); + values.put("000100", "E"); + values.put("000101", "F"); + values.put("000110", "G"); + values.put("000111", "H"); + values.put("001000", "I"); + values.put("001001", "J"); + values.put("001010", "K"); + values.put("001011", "L"); + values.put("001100", "M"); + values.put("001101", "N"); + values.put("001110", "O"); + values.put("001111", "P"); + values.put("010000", "Q"); + values.put("010001", "R"); + values.put("010010", "S"); + values.put("010011", "T"); + values.put("010100", "U"); + values.put("010101", "V"); + values.put("010110", "W"); + values.put("010111", "X"); + values.put("011000", "Y"); + values.put("011001", "Z"); + values.put("011010", "a"); + values.put("011011", "b"); + values.put("011100", "c"); + values.put("011101", "d"); + values.put("011110", "e"); + values.put("011111", "f"); + values.put("100000","g"); + values.put("100001","h"); + values.put("100010","i"); + values.put("100011","j"); + values.put("100100","k"); + values.put("100101","l"); + values.put("100110","m"); + values.put("100111","n"); + values.put("101000","o"); + values.put("101001","p"); + values.put("101010","q"); + values.put("101011","r"); + values.put("101100","s"); + values.put("101101","t"); + values.put("101110","u"); + values.put("101111","v"); + values.put("110000","w"); + values.put("110001","x"); + values.put("110010","y"); + values.put("110011","z"); + values.put("110100","0"); + values.put("110101","1"); + values.put("110110","2"); + values.put("110111","3"); + values.put("111000","4"); + values.put("111001","5"); + values.put("111010","6"); + values.put("111011","7"); + values.put("111100","8"); + values.put("111101","9"); + values.put("111110","+"); + values.put("111111","/"); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/HtmlEscape.java b/cloudinary-core/src/main/java/com/cloudinary/utils/HtmlEscape.java new file mode 100644 index 00000000..39ba901e --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/HtmlEscape.java @@ -0,0 +1,189 @@ +package com.cloudinary.utils; + + +/** + * HtmlEscape in Java, which is compatible with utf-8 + * + * @author Ulrich Jensen, http://www.htmlescape.net + * Feel free to get inspired, use or steal this code and use it in your + * own projects. + * License: + * You have the right to use this code in your own project or publish it + * on your own website. + * If you are going to use this code, please include the author lines. + * Use this code at your own risk. The author does not warrent or assume any + * legal liability or responsibility for the accuracy, completeness or usefullness of + * this program code. + */ + +public final class HtmlEscape { + private HtmlEscape() {} + + private static char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Method for html escaping a String, for use in a textarea + * + * @param original The String to escape + * @return The escaped String + */ + public static String escapeTextArea(String original) { + return escapeTags(escapeSpecial(original)); + } + + /** + * Normal escape function, for Html escaping Strings + * + * @param original The original String + * @return The escape String + */ + public static String escape(String original) { + return escapeBr(escapeTags(escapeSpecial(original))); + } + + public static String escapeTags(String original) { + if (original == null) return ""; + StringBuffer out = new StringBuffer(""); + char[] chars = original.toCharArray(); + for (int i = 0; i < chars.length; i++) { + boolean found = true; + switch (chars[i]) { + case 60: + out.append("<"); + break; //< + case 62: + out.append(">"); + break; //> + case 34: + out.append("""); + break; //" + default: + found = false; + break; + } + if (!found) out.append(chars[i]); + + } + return out.toString(); + + } + + public static String escapeBr(String original) { + if (original == null) return ""; + StringBuffer out = new StringBuffer(""); + char[] chars = original.toCharArray(); + for (int i = 0; i < chars.length; i++) { + boolean found = true; + switch (chars[i]) { + case '\n': + out.append("
"); + break; //newline + case '\r': + break; + default: + found = false; + break; + } + if (!found) out.append(chars[i]); + + } + return out.toString(); + } + + public static String escapeSpecial(String original) { + if (original == null) return ""; + StringBuffer out = new StringBuffer(""); + char[] chars = original.toCharArray(); + for (int i = 0; i < chars.length; i++) { + boolean found = true; + switch(chars[i]) { + // @formatter:off + case 38:out.append("&"); break; //& + case 198:out.append("Æ"); break; //Æ + case 193:out.append("Á"); break; //Á + case 194:out.append("Â"); break; //Â + case 192:out.append("À"); break; //À + case 197:out.append("Å"); break; //Å + case 195:out.append("Ã"); break; //Ã + case 196:out.append("Ä"); break; //Ä + case 199:out.append("Ç"); break; //Ç + case 208:out.append("Ð"); break; //Ð + case 201:out.append("É"); break; //É + case 202:out.append("Ê"); break; //Ê + case 200:out.append("È"); break; //È + case 203:out.append("Ë"); break; //Ë + case 205:out.append("Í"); break; //Í + case 206:out.append("Î"); break; //Î + case 204:out.append("Ì"); break; //Ì + case 207:out.append("Ï"); break; //Ï + case 209:out.append("Ñ"); break; //Ñ + case 211:out.append("Ó"); break; //Ó + case 212:out.append("Ô"); break; //Ô + case 210:out.append("Ò"); break; //Ò + case 216:out.append("Ø"); break; //Ø + case 213:out.append("Õ"); break; //Õ + case 214:out.append("Ö"); break; //Ö + case 222:out.append("Þ"); break; //Þ + case 218:out.append("Ú"); break; //Ú + case 219:out.append("Û"); break; //Û + case 217:out.append("Ù"); break; //Ù + case 220:out.append("Ü"); break; //Ü + case 221:out.append("Ý"); break; //Ý + case 225:out.append("á"); break; //á + case 226:out.append("â"); break; //â + case 230:out.append("æ"); break; //æ + case 224:out.append("à"); break; //à + case 229:out.append("å"); break; //å + case 227:out.append("ã"); break; //ã + case 228:out.append("ä"); break; //ä + case 231:out.append("ç"); break; //ç + case 233:out.append("é"); break; //é + case 234:out.append("ê"); break; //ê + case 232:out.append("è"); break; //è + case 240:out.append("ð"); break; //ð + case 235:out.append("ë"); break; //ë + case 237:out.append("í"); break; //í + case 238:out.append("î"); break; //î + case 236:out.append("ì"); break; //ì + case 239:out.append("ï"); break; //ï + case 241:out.append("ñ"); break; //ñ + case 243:out.append("ó"); break; //ó + case 244:out.append("ô"); break; //ô + case 242:out.append("ò"); break; //ò + case 248:out.append("ø"); break; //ø + case 245:out.append("õ"); break; //õ + case 246:out.append("ö"); break; //ö + case 223:out.append("ß"); break; //ß + case 254:out.append("þ"); break; //þ + case 250:out.append("ú"); break; //ú + case 251:out.append("û"); break; //û + case 249:out.append("ù"); break; //ù + case 252:out.append("ü"); break; //ü + case 253:out.append("ý"); break; //ý + case 255:out.append("ÿ"); break; //ÿ + case 162:out.append("¢"); break; //¢ + // @formatter:on + default: + found=false; + break; + } + if (!found) { + if (chars[i] > 127) { + char c = chars[i]; + int a4 = c % 16; + c = (char) (c / 16); + int a3 = c % 16; + c = (char) (c / 16); + int a2 = c % 16; + c = (char) (c / 16); + int a1 = c % 16; + out.append("&#x" + hex[a1] + hex[a2] + hex[a3] + hex[a4] + ";"); + } else { + out.append(chars[i]); + } + } + } + return out.toString(); + } + +} \ No newline at end of file diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java b/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java new file mode 100644 index 00000000..2dc607f6 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/ObjectUtils.java @@ -0,0 +1,231 @@ +package com.cloudinary.utils; + +import org.cloudinary.json.JSONArray; +import org.cloudinary.json.JSONException; +import org.cloudinary.json.JSONObject; + +import java.io.*; +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; + + +public final class ObjectUtils { + private ObjectUtils() {} + + /** + * Formats a Date as an ISO-8601 string representation. + * @param date Date to format + * @return The date formatted as ISO-8601 string + */ + public static String toISO8601(Date date){ + DateFormat dateFormat = getDateFormat(); + return dateFormat.format(date); + } + + public static Date fromISO8601(String date) throws ParseException { + DateFormat dateFormat = getDateFormat(); + return (Date) dateFormat.parseObject(date); + } + + private static DateFormat getDateFormat() { + DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.US); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat; + } + + public static String asString(Object value) { + if (value == null) { + return null; + } else { + return value.toString(); + } + } + + public static String asString(Object value, String defaultValue) { + if (value == null) { + return defaultValue; + } else { + return value.toString(); + } + } + + public static String serialize(Object object) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ObjectOutputStream objectOutputStream = new ObjectOutputStream(baos); + try { + objectOutputStream.writeObject(object); + return new String(Base64Coder.encode(baos.toByteArray())); + } finally { + objectOutputStream.close(); + } + } + + public static Object deserialize(String base64SerializedString) throws IOException, ClassNotFoundException { + byte[] buf = Base64Coder.decode(base64SerializedString); + return new ObjectInputStream(new ByteArrayInputStream(buf)).readObject(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static List asArray(Object value) { + if (value == null) { + return Collections.EMPTY_LIST; + } else if (value instanceof int[]) { + List array = new ArrayList(); + for (int i : (int[]) value) { + array.add(new Integer(i)); + } + return array; + } else if (value instanceof Object[]) { + return Arrays.asList((Object[]) value); + } else if (value instanceof List) { + return (List) value; + } else { + List array = new ArrayList(); + array.add(value); + return array; + } + } + + public static Boolean asBoolean(Object value, Boolean defaultValue) { + if (value == null) { + return defaultValue; + } else return asBoolean(value); + } + + public static Boolean asBoolean(Object value) { + if (value instanceof Boolean) { + return (Boolean) value; + } else { + return "true".equals(value); + } + } + + public static Float asFloat(Object value) { + if (value == null) { + return null; + } else if (value instanceof Float) { + return (Float) value; + } else { + return Float.parseFloat(value.toString()); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static Map asMap(Object... values) { + if (values.length % 2 != 0) + throw new RuntimeException("Usage - (key, value, key, value, ...)"); + Map result = new HashMap(values.length / 2); + for (int i = 0; i < values.length; i += 2) { + result.put(values[i], values[i + 1]); + } + return result; + } + + @SuppressWarnings("rawtypes") + public static Map emptyMap() { + return Collections.EMPTY_MAP; + } + + @SuppressWarnings({"unchecked", "rawtypes"}) + public static String encodeMap(Object arg) { + if (arg != null && arg instanceof Map) { + Map mapArg = (Map) arg; + HashSet out = new HashSet(); + for (Map.Entry entry : mapArg.entrySet()) { + out.add(entry.getKey() + "=" + entry.getValue()); + } + return StringUtils.join(out.toArray(), "|"); + } else if (arg == null) { + return null; + } else { + return arg.toString(); + } + } + + public static Map only(Map hash, String... keys) { + Map result = new HashMap(); + for (String key : keys) { + if (hash.containsKey(key)) { + result.put(key, hash.get(key)); + } + } + return result; + } + + @SuppressWarnings("rawtypes") + public static Map toMap(JSONObject object) throws JSONException { + @SuppressWarnings("unchecked") + Map map = new HashMap(); + Iterator keys = object.keys(); + while (keys.hasNext()) { + String key = (String) keys.next(); + map.put(key, fromJson(object.get(key))); + } + return map; + } + + public static JSONObject toJSON(Map map) throws JSONException { + JSONObject json = new JSONObject(); + for (Map.Entry entry : map.entrySet()) { + String field = entry.getKey(); + Object value = entry.getValue(); + json.put(field, value); + } + return json; + } + + private static Object fromJson(Object json) throws JSONException { + if (json == JSONObject.NULL) { + return null; + } else if (json instanceof JSONObject) { + return toMap((JSONObject) json); + } else if (json instanceof JSONArray) { + return toList((JSONArray) json); + } else { + return json; + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public static List toList(JSONArray array) throws JSONException { + List list = new ArrayList(); + for (int i = 0; i < array.length(); i++) { + list.add(fromJson(array.get(i))); + } + return list; + } + + public static Integer asInteger(Object value, Integer defaultValue) { + if (value == null) { + return defaultValue; + } else if (value instanceof Integer) { + return (Integer) value; + } else { + return Integer.parseInt(value.toString()); + } + } + + public static Long asLong(Object value, Long defaultValue) { + if (value == null) { + return defaultValue; + } else if (value instanceof Long) { + return (Long) value; + } else { + return Long.parseLong(value.toString()); + } + } + + public static String toUsageApiDateFormat(Date date){ + return new SimpleDateFormat("dd-MM-yyy").format(date); + } + + public static String toISO8601DateOnly(Date date) { + return new SimpleDateFormat("yyyy-MM-dd").format(date); + } + + public static Date fromISO8601DateOnly(String string) throws ParseException { + return new SimpleDateFormat("yyyy-MM-dd").parse(string); + } +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/Rectangle.java b/cloudinary-core/src/main/java/com/cloudinary/utils/Rectangle.java new file mode 100644 index 00000000..698af43e --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/Rectangle.java @@ -0,0 +1,19 @@ +package com.cloudinary.utils; + +import java.io.Serializable; + +public class Rectangle implements Serializable{ + + public int height; + public int width; + public int y; + public int x; + + public Rectangle(int x, int y, int width, int height) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + +} diff --git a/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java b/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java new file mode 100644 index 00000000..f8a21231 --- /dev/null +++ b/cloudinary-core/src/main/java/com/cloudinary/utils/StringUtils.java @@ -0,0 +1,452 @@ +package com.cloudinary.utils; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.Charset; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class StringUtils { + private StringUtils() {} + + public static final String EMPTY = ""; + + /** + * Join a list of Strings + * + * @param list strings to join + * @param separator the separator to insert between the strings + * @return a string made of the strings in list separated by separator + */ + public static String join(List list, String separator) { + if (list == null) { + return null; + } + + return join(list.toArray(), separator, 0, list.size()); + } + + /** + * Join a array of Strings + * + * @param array strings to join + * @param separator the separator to insert between the strings + * @return a string made of the strings in array separated by separator + */ + public static String join(Object[] array, String separator) { + if (array == null) { + return null; + } + return join(array, separator, 0, array.length); + } + + /** + * Join a collection of Strings + * + * @param collection strings to join + * @param separator the separator to insert between the strings + * @return a string made of the strings in collection separated by separator + */ + public static String join(Collection collection, String separator) { + if (collection == null) { + return null; + } + return join(collection.toArray(new String[collection.size()]), separator, 0, collection.size()); + } + + /** + * Join a array of Strings from startIndex to endIndex + * + * @param array strings to join + * @param separator the separator to insert between the strings + * @param startIndex the string to start from + * @param endIndex the last string to join + * @return a string made of the strings in array separated by separator + */ + public static String join(final Object[] array, String separator, final int startIndex, final int endIndex) { + if (array == null) { + return null; + } + if (separator == null) { + separator = EMPTY; + } + + final int noOfItems = endIndex - startIndex; + if (noOfItems <= 0) { + return EMPTY; + } + + final StringBuilder buf = new StringBuilder(noOfItems * 16); + + for (int i = startIndex; i < endIndex; i++) { + if (i > startIndex) { + buf.append(separator); + } + if (array[i] != null) { + buf.append(array[i]); + } + } + return buf.toString(); + } + + final protected static char[] hexArray = "0123456789abcdef".toCharArray(); + + /** + * Convert an array of bytes to a string of hex values + * + * @param bytes bytes to convert + * @return a string of hex values. + */ + public static String encodeHexString(byte[] bytes) { + char[] hexChars = new char[bytes.length * 2]; + for (int j = 0; j < bytes.length; j++) { + int v = bytes[j] & 0xFF; + hexChars[j * 2] = hexArray[v >>> 4]; + hexChars[j * 2 + 1] = hexArray[v & 0x0F]; + } + return new String(hexChars); + } + + /** + * Convert a string of hex values to an array of bytes + * + * @param s a string of two digit Hex numbers. The length of string to parse must be even. + * @return bytes representation of the string + */ + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + + if (len % 2 != 0) { + throw new IllegalArgumentException("Length of string to parse must be even."); + } + + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + + return data; + } + + /** + * Method for html escaping a String + * + * @param input The String to escape + * @return The escaped String + * @see HtmlEscape#escapeTextArea(String) + */ + public static String escapeHtml(String input) { + return HtmlEscape.escapeTextArea(input); + } + + /** + * Verify that the input has non whitespace characters in it + * + * @param input a String-like object + * @return true if input has non whitespace characters in it + */ + public static boolean isNotBlank(Object input) { + if (input == null) return false; + return !isBlank(input.toString()); + } + + /** + * Verify that the input has non whitespace characters in it + * + * @param input a String + * @return true if input has non whitespace characters in it + */ + public static boolean isNotBlank(String input) { + return !isBlank(input); + } + + /** + * Verify that the input has no characters + * + * @param input a string + * @return true if input is null or has no characters + */ + public static boolean isEmpty(String input) { + return input == null || input.length() == 0; + } + + /** + * Verify that the input is an empty string or contains only whitespace characters.
+ * see {@link Character#isWhitespace(char)} + * + * @param input a string + * @return true if input is an empty string or contains only whitespace characters + */ + public static boolean isBlank(String input) { + int strLen; + if (input == null || (strLen = input.length()) == 0) { + return true; + } + for (int i = 0; i < strLen; i++) { + if (!Character.isWhitespace(input.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Read the entire input stream in 1KB chunks + * + * @param in input stream to read from + * @return a String generated from the input stream + * @throws IOException thrown by the input stream + */ + public static String read(InputStream in) throws IOException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int length = 0; + while ((length = in.read(buffer)) != -1) { + baos.write(buffer, 0, length); + } + return new String(baos.toByteArray()); + } + + public static boolean isRemoteUrl(String file) { + return file.matches("ftp:.*|https?:.*|s3:.*|gs:.*|data:([\\w-]+/[\\w-]+(\\+[\\w-]+)?)?(;[\\w-]+=[\\w-]+)*;base64,([a-zA-Z0-9/+\n=]+)"); + } + + /** + * Replaces the unsafe characters in url with url-encoded values. + * This is based on {@link java.net.URLEncoder#encode(String, String)} + * @param url The url to encode + * @param unsafe Regex pattern of unsafe characters + * @param charset + * @return An encoded url string + */ + public static String urlEncode(String url, Pattern unsafe, Charset charset) { + StringBuffer sb = new StringBuffer(url.length()); + Matcher matcher = unsafe.matcher(url); + while (matcher.find()) { + String str = matcher.group(0); + byte[] bytes = str.getBytes(charset); + StringBuilder escaped = new StringBuilder(str.length() * 3); + + for (byte aByte : bytes) { + escaped.append('%'); + char ch = Character.forDigit((aByte >> 4) & 0xF, 16); + escaped.append(ch); + ch = Character.forDigit(aByte & 0xF, 16); + escaped.append(ch); + } + + matcher.appendReplacement(sb, Matcher.quoteReplacement(escaped.toString().toLowerCase())); + } + + matcher.appendTail(sb); + return sb.toString(); + } + + /** + * Merge all consecutive underscores and spaces into a single underscore, e.g. "ab___c_ _d" becomes "ab_c_d" + * + * @param s String to process + * @return The resulting string. + */ + public static String mergeToSingleUnderscore(String s) { + StringBuffer buffer = new StringBuffer(); + boolean inMerge = false; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == ' ' || c == '_') { + if (!inMerge) { + buffer.append('_'); + } + inMerge = true; + + } else { + inMerge = false; + buffer.append(c); + } + } + + return buffer.toString(); + } + + /** + * Checks whether the String fits the template for a transformation variable - $[a-zA-Z][a-zA-Z0-9]+ + * e.g. $a4, $Bd, $abcdef, etc + * + * @param s The string to test + * @return Whether it's a variable or not + */ + public static boolean isVariable(String s) { + if (s == null || + s.length() < 2 || + !s.startsWith("$") || + !Character.isLetter(s.charAt(1))) { + return false; + } + + // check that the rest of the string is comprised of letters and digits only: + for (int i = 2; i < s.length(); i++) { + char c = s.charAt(i); + if (!Character.isLetterOrDigit(c)) { + return false; + } + } + + return true; + } + + /** + * Replaces the char c in the string S, if it's the first character in the string. + * @param s The string to search + * @param c The character to replace + * @param replacement The string to replace the character in S + * @return The string with the character replaced (or the original string if the char is not found) + */ + public static String replaceIfFirstChar(String s, char c, String replacement) { + return s.charAt(0) == c ? replacement + s.substring(1) : s; + } + + /** + * Check if the given string starts with http:// or https:// + * @param s The string to check + * @return Whether it's an http url or not + */ + public static boolean isHttpUrl(String s) { + String lowerCaseSource = s.toLowerCase(); + return lowerCaseSource.startsWith("https:/") || lowerCaseSource.startsWith("http:/"); + } + + /** + * Remove all consecutive chars c from the beginning of the string + * @param s String to process + * @param c Char to search for + * @return The string stripped from the starting chars. + */ + public static String removeStartingChars(String s, char c) { + int lastToRemove = -1; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == c) { + lastToRemove = i; + continue; + } + + if (s.charAt(i) != c) { + break; + } + } + + if (lastToRemove < 0) return s; + return s.substring(lastToRemove + 1); + } + + /** + * Checks whether a publicId starts a versioning string (v + number, e.g. v12345) + * @param publicId The url to check + * @return Whether a version string is contained within the publicId + */ + public static boolean startWithVersionString(String publicId){ + if (publicId.startsWith("/")){ + publicId = publicId.substring(1); + } + return publicId.length()>1 && publicId.startsWith("v") && Character.isDigit(publicId.charAt(1)); + } + + /** + * Merges all occurrences of multiple slashes into a single slash (e.g. "a///b//c/d" becomes "a/b/c/d") + * @param url The string to process + * @return The resulting string with merged slashes. + */ + public static String mergeSlashesInUrl(String url) { + StringBuilder builder = new StringBuilder(); + boolean prevIsColon = false; + boolean inMerge = false; + for (int i = 0; i < url.length(); i++) { + char c = url.charAt(i); + if (c == ':') { + prevIsColon = true; + builder.append(c); + } else { + if (c == '/') { + if (prevIsColon) { + builder.append(c); + inMerge = false; + } else { + if (!inMerge) { + builder.append(c); + } + inMerge = true; + } + } else { + inMerge = false; + builder.append(c); + } + + prevIsColon = false; + } + } + + return builder.toString(); + } + + /** + * Returns empty string value when passed string value is null or empty, the passed string itself otherwise. + * + * @param str string value to evaluate + * @return passed string value or empty string, if the passed string is null or empty + */ + public static String emptyIfNull(String str) { + return isEmpty(str) ? "" : str; + } + + /** + * Returns an array of strings in reveresed order. + * + * @param strings array of strings + * @return reversed array of string or empty array, if the passed array is null or empty + */ + static String[] reverseStringArray(String[] strings) { + Collections.reverse(Arrays.asList(strings)); + return strings; + } + + /** + * Returns the padded string with requested character to the left with length equals to length param sent. + * + * @param inputString The string to process + * @param length The requested length to pad to + * @param paddingCharacter The requested character to pad with + * @return reversed array of string or empty array, if the passed array is null or empty + */ + public static String padStart(String inputString, int length, char paddingCharacter) { + if (inputString.length() >= length) { + return inputString; + } + StringBuilder sb = new StringBuilder(); + while (sb.length() < length - inputString.length()) { + sb.append(paddingCharacter); + } + sb.append(inputString); + + return sb.toString(); + } + + /** + * Break string into groups of n size strings + * + * @param text The string to process + * @param n Size of group + * @return List with all strings with group size n. + */ + public static List getAllSubStringWithSize(String text, int n) { + List results = new ArrayList<>(); + + Pattern pattern = Pattern.compile(".{1," + n + "}"); + Matcher matcher = pattern.matcher(text); + while (matcher.find()) { + String match = text.substring(matcher.start(), matcher.end()); + results.add(match); + } + return results; + } +} diff --git a/cloudinary-core/src/main/java/org/cloudinary/json/JSONArray.java b/cloudinary-core/src/main/java/org/cloudinary/json/JSONArray.java new file mode 100644 index 00000000..82f7d19b --- /dev/null +++ b/cloudinary-core/src/main/java/org/cloudinary/json/JSONArray.java @@ -0,0 +1,895 @@ +package org.cloudinary.json; + +/* + Copyright (c) 2002 JSON.org + + 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 shall be used for Good, not Evil. + + 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. + */ + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Array; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; + +/** + * A JSONArray is an ordered sequence of values. Its external text form is a + * string wrapped in square brackets with commas separating the values. The + * internal form is an object having get and opt + * methods for accessing the values by index, and put methods for + * adding or replacing values. The values can be any of these types: + * Boolean, JSONArray, JSONObject, + * Number, String, or the + * JSONObject.NULL object. + *

+ * The constructor can convert a JSON text into a Java object. The + * toString method converts to JSON text. + *

+ * A get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. + *

+ * The texts produced by the toString methods strictly conform to + * JSON syntax rules. The constructors are more forgiving in the texts they will + * accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing bracket.
  • + *
  • The null value will be inserted when there is , + *  (comma) elision.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a quote + * or single quote, and if they do not contain leading or trailing spaces, and + * if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and if + * they are not the reserved words true, false, or + * null.
  • + *
+ * + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONArray implements Serializable { + + /** + * The arrayList where the JSONArray's properties are kept. + */ + private final ArrayList myArrayList; + + /** + * Construct an empty JSONArray. + */ + public JSONArray() { + this.myArrayList = new ArrayList(); + } + + /** + * Construct a JSONArray from a JSONTokener. + * + * @param x A JSONTokener + * @throws JSONException If there is a syntax error. + */ + public JSONArray(JSONTokener x) throws JSONException { + this(); + if (x.nextClean() != '[') { + throw x.syntaxError("A JSONArray text must start with '['"); + } + if (x.nextClean() != ']') { + x.back(); + for (; ; ) { + if (x.nextClean() == ',') { + x.back(); + this.myArrayList.add(JSONObject.NULL); + } else { + x.back(); + this.myArrayList.add(x.nextValue()); + } + switch (x.nextClean()) { + case ',': + if (x.nextClean() == ']') { + return; + } + x.back(); + break; + case ']': + return; + default: + throw x.syntaxError("Expected a ',' or ']'"); + } + } + } + } + + /** + * Construct a JSONArray from a source JSON text. + * + * @param source A string that begins with [ (left + * bracket) and ends with ] + *  (right bracket). + * @throws JSONException If there is a syntax error. + */ + public JSONArray(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONArray from a Collection. + * + * @param collection A Collection. + */ + public JSONArray(Collection collection) { + this.myArrayList = new ArrayList(); + if (collection != null) { + Iterator iter = collection.iterator(); + while (iter.hasNext()) { + this.myArrayList.add(JSONObject.wrap(iter.next())); + } + } + } + + /** + * Construct a JSONArray from an array + * + * @throws JSONException If not an array. + */ + public JSONArray(Object array) throws JSONException { + this(); + if (array.getClass().isArray()) { + int length = Array.getLength(array); + for (int i = 0; i < length; i += 1) { + this.put(JSONObject.wrap(Array.get(array, i))); + } + } else { + throw new JSONException("JSONArray initial value should be a string or collection or array."); + } + } + + /** + * Get the object value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return An object value. + * @throws JSONException If there is no value for the index. + */ + public Object get(int index) throws JSONException { + Object object = this.opt(index); + if (object == null) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with an index. The string values "true" + * and "false" are converted to boolean. + * + * @param index The index must be between 0 and length() - 1. + * @return The truth. + * @throws JSONException If there is no value for the index or if the value is not + * convertible to boolean. + */ + public boolean getBoolean(int index) throws JSONException { + Object object = this.get(index); + if (object.equals(Boolean.FALSE) || (object instanceof String && ((String) object).equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) || (object instanceof String && ((String) object).equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONArray[" + index + "] is not a boolean."); + } + + /** + * Get the double value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot be converted + * to a number. + */ + public double getDouble(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number ? ((Number) object).doubleValue() : Double.parseDouble((String) object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number."); + } + } + + /** + * Get the int value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value is not a number. + */ + public int getInt(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number ? ((Number) object).intValue() : Integer.parseInt((String) object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number."); + } + } + + /** + * Get the JSONArray associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return A JSONArray value. + * @throws JSONException If there is no value for the index. or if the value is not a + * JSONArray + */ + public JSONArray getJSONArray(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONArray[" + index + "] is not a JSONArray."); + } + + /** + * Get the JSONObject associated with an index. + * + * @param index subscript + * @return A JSONObject value. + * @throws JSONException If there is no value for the index or if the value is not a + * JSONObject + */ + public JSONObject getJSONObject(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONArray[" + index + "] is not a JSONObject."); + } + + /** + * Get the long value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + * @throws JSONException If the key is not found or if the value cannot be converted + * to a number. + */ + public long getLong(int index) throws JSONException { + Object object = this.get(index); + try { + return object instanceof Number ? ((Number) object).longValue() : Long.parseLong((String) object); + } catch (Exception e) { + throw new JSONException("JSONArray[" + index + "] is not a number."); + } + } + + /** + * Get the string associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return A string value. + * @throws JSONException If there is no string value for the index. + */ + public String getString(int index) throws JSONException { + Object object = this.get(index); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONArray[" + index + "] not a string."); + } + + /** + * Determine if the value is null. + * + * @param index The index must be between 0 and length() - 1. + * @return true if the value at the index is null, or if there is no value. + */ + public boolean isNull(int index) { + return JSONObject.NULL.equals(this.opt(index)); + } + + /** + * Make a string from the contents of this JSONArray. The + * separator string is inserted between each element. Warning: + * This method assumes that the data structure is acyclical. + * + * @param separator A string that will be inserted between the elements. + * @return a string. + * @throws JSONException If the array contains an invalid number. + */ + public String join(String separator) throws JSONException { + int len = this.length(); + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < len; i += 1) { + if (i > 0) { + sb.append(separator); + } + sb.append(JSONObject.valueToString(this.myArrayList.get(i))); + } + return sb.toString(); + } + + /** + * Get the number of elements in the JSONArray, included nulls. + * + * @return The length (or size). + */ + public int length() { + return this.myArrayList.size(); + } + + /** + * Get the optional object value associated with an index. + * + * @param index The index must be between 0 and length() - 1. + * @return An object value, or null if there is no object at that index. + */ + public Object opt(int index) { + return (index < 0 || index >= this.length()) ? null : this.myArrayList.get(index); + } + + /** + * Get the optional boolean value associated with an index. It returns false + * if there is no value at that index, or if the value is not Boolean.TRUE + * or the String "true". + * + * @param index The index must be between 0 and length() - 1. + * @return The truth. + */ + public boolean optBoolean(int index) { + return this.optBoolean(index, false); + } + + /** + * Get the optional boolean value associated with an index. It returns the + * defaultValue if there is no value at that index or if it is not a Boolean + * or the String "true" or "false" (case insensitive). + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue A boolean default. + * @return The truth. + */ + public boolean optBoolean(int index, boolean defaultValue) { + try { + return this.getBoolean(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional double value associated with an index. NaN is returned + * if there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public double optDouble(int index) { + return this.optDouble(index, Double.NaN); + } + + /** + * Get the optional double value associated with an index. The defaultValue + * is returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index subscript + * @param defaultValue The default value. + * @return The value. + */ + public double optDouble(int index, double defaultValue) { + try { + return this.getDouble(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional int value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public int optInt(int index) { + return this.optInt(index, 0); + } + + /** + * Get the optional int value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public int optInt(int index, int defaultValue) { + try { + return this.getInt(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional JSONArray associated with an index. + * + * @param index subscript + * @return A JSONArray value, or null if the index has no value, or if the + * value is not a JSONArray. + */ + public JSONArray optJSONArray(int index) { + Object o = this.opt(index); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get the optional JSONObject associated with an index. Null is returned if + * the key is not found, or null if the index has no value, or if the value + * is not a JSONObject. + * + * @param index The index must be between 0 and length() - 1. + * @return A JSONObject value. + */ + public JSONObject optJSONObject(int index) { + Object o = this.opt(index); + return o instanceof JSONObject ? (JSONObject) o : null; + } + + /** + * Get the optional long value associated with an index. Zero is returned if + * there is no value for the index, or if the value is not a number and + * cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @return The value. + */ + public long optLong(int index) { + return this.optLong(index, 0); + } + + /** + * Get the optional long value associated with an index. The defaultValue is + * returned if there is no value for the index, or if the value is not a + * number and cannot be converted to a number. + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return The value. + */ + public long optLong(int index, long defaultValue) { + try { + return this.getLong(index); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get the optional string value associated with an index. It returns an + * empty string if there is no value at that index. If the value is not a + * string and is not null, then it is coverted to a string. + * + * @param index The index must be between 0 and length() - 1. + * @return A String value. + */ + public String optString(int index) { + return this.optString(index, ""); + } + + /** + * Get the optional string associated with an index. The defaultValue is + * returned if the key is not found. + * + * @param index The index must be between 0 and length() - 1. + * @param defaultValue The default value. + * @return A String value. + */ + public String optString(int index, String defaultValue) { + Object object = this.opt(index); + return JSONObject.NULL.equals(object) ? defaultValue : object.toString(); + } + + /** + * Append a boolean value. This increases the array's length by one. + * + * @param value A boolean value. + * @return this. + */ + public JSONArray put(boolean value) { + this.put(value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param value A Collection value. + * @return this. + */ + public JSONArray put(Collection value) { + this.put(new JSONArray(value)); + return this; + } + + /** + * Append a double value. This increases the array's length by one. + * + * @param value A double value. + * @return this. + * @throws JSONException if the value is not finite. + */ + public JSONArray put(double value) throws JSONException { + Double d = new Double(value); + JSONObject.testValidity(d); + this.put(d); + return this; + } + + /** + * Append an int value. This increases the array's length by one. + * + * @param value An int value. + * @return this. + */ + public JSONArray put(int value) { + this.put(new Integer(value)); + return this; + } + + /** + * Append an long value. This increases the array's length by one. + * + * @param value A long value. + * @return this. + */ + public JSONArray put(long value) { + this.put(new Long(value)); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject which + * is produced from a Map. + * + * @param value A Map value. + * @return this. + */ + public JSONArray put(Map value) { + this.put(new JSONObject(value)); + return this; + } + + /** + * Append an object value. This increases the array's length by one. + * + * @param value An object value. The value should be a Boolean, Double, + * Integer, JSONArray, JSONObject, Long, or String, or the + * JSONObject.NULL object. + * @return this. + */ + public JSONArray put(Object value) { + this.myArrayList.add(value); + return this; + } + + /** + * Put or replace a boolean value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index The subscript. + * @param value A boolean value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, boolean value) throws JSONException { + this.put(index, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONArray which + * is produced from a Collection. + * + * @param index The subscript. + * @param value A Collection value. + * @return this. + * @throws JSONException If the index is negative or if the value is not finite. + */ + public JSONArray put(int index, Collection value) throws JSONException { + this.put(index, new JSONArray(value)); + return this; + } + + /** + * Put or replace a double value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index The subscript. + * @param value A double value. + * @return this. + * @throws JSONException If the index is negative or if the value is not finite. + */ + public JSONArray put(int index, double value) throws JSONException { + this.put(index, new Double(value)); + return this; + } + + /** + * Put or replace an int value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index The subscript. + * @param value An int value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, int value) throws JSONException { + this.put(index, new Integer(value)); + return this; + } + + /** + * Put or replace a long value. If the index is greater than the length of + * the JSONArray, then null elements will be added as necessary to pad it + * out. + * + * @param index The subscript. + * @param value A long value. + * @return this. + * @throws JSONException If the index is negative. + */ + public JSONArray put(int index, long value) throws JSONException { + this.put(index, new Long(value)); + return this; + } + + /** + * Put a value in the JSONArray, where the value will be a JSONObject that + * is produced from a Map. + * + * @param index The subscript. + * @param value The Map value. + * @return this. + * @throws JSONException If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Map value) throws JSONException { + this.put(index, new JSONObject(value)); + return this; + } + + /** + * Put or replace an object value in the JSONArray. If the index is greater + * than the length of the JSONArray, then null elements will be added as + * necessary to pad it out. + * + * @param index The subscript. + * @param value The value to put into the array. The value should be a + * Boolean, Double, Integer, JSONArray, JSONObject, Long, or + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the index is negative or if the the value is an invalid + * number. + */ + public JSONArray put(int index, Object value) throws JSONException { + JSONObject.testValidity(value); + if (index < 0) { + throw new JSONException("JSONArray[" + index + "] not found."); + } + if (index < this.length()) { + this.myArrayList.set(index, value); + } else { + while (index != this.length()) { + this.put(JSONObject.NULL); + } + this.put(value); + } + return this; + } + + /** + * Remove an index and close the hole. + * + * @param index The index of the element to be removed. + * @return The value that was associated with the index, or null if there + * was no value. + */ + public Object remove(int index) { + return index >= 0 && index < this.length() ? this.myArrayList.remove(index) : null; + } + + /** + * Determine if two JSONArrays are similar. They must contain similar + * sequences. + * + * @param other The other JSONArray + * @return true if they are equal + */ + public boolean similar(Object other) { + if (!(other instanceof JSONArray)) { + return false; + } + int len = this.length(); + if (len != ((JSONArray) other).length()) { + return false; + } + for (int i = 0; i < len; i += 1) { + Object valueThis = this.get(i); + Object valueOther = ((JSONArray) other).get(i); + if (valueThis instanceof JSONObject) { + if (!((JSONObject) valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray) valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } + + /** + * Produce a JSONObject by combining a JSONArray of names with the values of + * this JSONArray. + * + * @param names A JSONArray containing a list of key strings. These will be + * paired with the values. + * @return A JSONObject, or null if there are no names or if this JSONArray + * has no values. + * @throws JSONException If any of the names are null. + */ + public JSONObject toJSONObject(JSONArray names) throws JSONException { + if (names == null || names.length() == 0 || this.length() == 0) { + return null; + } + JSONObject jo = new JSONObject(); + for (int i = 0; i < names.length(); i += 1) { + jo.put(names.getString(i), this.opt(i)); + } + return jo; + } + + /** + * Make a JSON text of this JSONArray. For compactness, no unnecessary + * whitespace is added. If it is not possible to produce a syntactically + * correct JSON text then null will be returned instead. This could occur if + * the array contains an invalid number. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, transmittable representation of the + * array. + */ + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a prettyprinted JSON text of this JSONArray. Warning: This method + * assumes that the data structure is acyclical. + * + * @param indentFactor The number of spaces to add to each level of indentation. + * @return a printable, displayable, transmittable representation of the + * object, beginning with [ (left + * bracket) and ending with ] + *  (right bracket). + * @throws JSONException + */ + public String toString(int indentFactor) throws JSONException { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + return this.write(sw, indentFactor, 0).toString(); + } + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + /** + * Write the contents of the JSONArray as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param indentFactor The number of spaces to add to each level of indentation. + * @param indent The indention of the top level. + * @return The writer. + * @throws JSONException + */ + Writer write(Writer writer, int indentFactor, int indent) throws JSONException { + try { + boolean commanate = false; + int length = this.length(); + writer.write('['); + + if (length == 1) { + JSONObject.writeValue(writer, this.myArrayList.get(0), indentFactor, indent); + } else if (length != 0) { + final int newindent = indent + indentFactor; + + for (int i = 0; i < length; i += 1) { + if (commanate) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, newindent); + JSONObject.writeValue(writer, this.myArrayList.get(i), indentFactor, newindent); + commanate = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + JSONObject.indent(writer, indent); + } + writer.write(']'); + return writer; + } catch (IOException e) { + throw new JSONException(e); + } + } + + @SuppressWarnings("unchecked") + public ArrayList toList(Class type) { + ArrayList listdata = new ArrayList(); + for (int i = 0; i < this.length(); i++) { + listdata.add((T) this.get(i)); + } + return listdata; + } + +} diff --git a/cloudinary-core/src/main/java/org/cloudinary/json/JSONException.java b/cloudinary-core/src/main/java/org/cloudinary/json/JSONException.java new file mode 100644 index 00000000..5fe33487 --- /dev/null +++ b/cloudinary-core/src/main/java/org/cloudinary/json/JSONException.java @@ -0,0 +1,43 @@ +package org.cloudinary.json; + +/** + * The JSONException is thrown by the JSON.org classes when things are amiss. + * + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONException extends RuntimeException { + private static final long serialVersionUID = 0; + private Throwable cause; + + /** + * Constructs a JSONException with an explanatory message. + * + * @param message Detail about the reason for the exception. + */ + public JSONException(String message) { + super(message); + } + + /** + * Constructs a new JSONException with the specified cause. + * + * @param cause The cause. + */ + public JSONException(Throwable cause) { + super(cause.getMessage()); + this.cause = cause; + } + + /** + * Returns the cause of this exception or null if the cause is nonexistent + * or unknown. + * + * @return the cause of this exception or null if the cause is nonexistent + * or unknown. + */ + @Override + public Throwable getCause() { + return this.cause; + } +} diff --git a/cloudinary-core/src/main/java/org/cloudinary/json/JSONObject.java b/cloudinary-core/src/main/java/org/cloudinary/json/JSONObject.java new file mode 100644 index 00000000..ef0e0b2d --- /dev/null +++ b/cloudinary-core/src/main/java/org/cloudinary/json/JSONObject.java @@ -0,0 +1,1593 @@ +package org.cloudinary.json; + +/* + Copyright (c) 2002 JSON.org + + 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 shall be used for Good, not Evil. + + 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. + */ + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.ResourceBundle; +import java.util.Set; + +/** + * A JSONObject is an unordered collection of name/value pairs. Its external + * form is a string wrapped in curly braces with colons between the names and + * values, and commas between the values and names. The internal form is an + * object having get and opt methods for accessing + * the values by name, and put methods for adding or replacing + * values by name. The values can be any of these types: Boolean, + * JSONArray, JSONObject, Number, + * String, or the JSONObject.NULL object. A + * JSONObject constructor can be used to convert an external form JSON text + * into an internal form whose values can be retrieved with the + * get and opt methods, or to convert values into a + * JSON text using the put and toString methods. A + * get method returns a value if one can be found, and throws an + * exception if one cannot be found. An opt method returns a + * default value instead of throwing an exception, and so is useful for + * obtaining optional values. + *

+ * The generic get() and opt() methods return an + * object, which you can cast or query for type. There are also typed + * get and opt methods that do type checking and type + * coercion for you. The opt methods differ from the get methods in that they + * do not throw. Instead, they return a specified value, such as null. + *

+ * The put methods add or replace values in an object. For + * example, + *

+ *

+ * myString = new JSONObject()
+ *         .put("JSON", "Hello, World!").toString();
+ * 
+ *

+ * produces the string {"JSON": "Hello, World"}. + *

+ * The texts produced by the toString methods strictly conform to + * the JSON syntax rules. The constructors are more forgiving in the texts they + * will accept: + *

    + *
  • An extra , (comma) may appear just + * before the closing brace.
  • + *
  • Strings may be quoted with ' (single + * quote).
  • + *
  • Strings do not need to be quoted at all if they do not begin with a + * quote or single quote, and if they do not contain leading or trailing + * spaces, and if they do not contain any of these characters: + * { } [ ] / \ : , # and if they do not look like numbers and + * if they are not the reserved words true, false, + * or null.
  • + *
+ * + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONObject implements Serializable{ + /** + * JSONObject.NULL is equivalent to the value that JavaScript calls null, + * whilst Java's null is equivalent to the value that JavaScript calls + * undefined. + */ + private static final class Null { + + /** + * There is only intended to be a single instance of the NULL object, + * so the clone method returns itself. + * + * @return NULL. + */ + @Override + protected final Object clone() { + return this; + } + + /** + * A Null object is equal to the null value and to itself. + * + * @param object An object to test for nullness. + * @return true if the object parameter is the JSONObject.NULL object or + * null. + */ + @Override + public boolean equals(Object object) { + return object == null || object == this; + } + + /** + * Get the "null" string value. + * + * @return The string "null". + */ + public String toString() { + return "null"; + } + } + + /** + * The map where the JSONObject's properties are kept. + */ + private final Map map; + + /** + * It is sometimes more convenient and less ambiguous to have a + * NULL object than to use Java's null value. + * JSONObject.NULL.equals(null) returns true. + * JSONObject.NULL.toString() returns "null". + */ + public static final Object NULL = new Null(); + + /** + * Construct an empty JSONObject. + */ + public JSONObject() { + this.map = new HashMap(); + } + + /** + * Construct a JSONObject from a subset of another JSONObject. An array of + * strings is used to identify the keys that should be copied. Missing keys + * are ignored. + * + * @param jo A JSONObject. + * @param names An array of strings. + * @throws JSONException + * @throws JSONException If a value is a non-finite number or if a name is + * duplicated. + */ + public JSONObject(JSONObject jo, String[] names) { + this(); + for (int i = 0; i < names.length; i += 1) { + try { + this.putOnce(names[i], jo.opt(names[i])); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a JSONTokener. + * + * @param x A JSONTokener object containing the source string. + * @throws JSONException If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(JSONTokener x) throws JSONException { + this(); + char c; + String key; + + if (x.nextClean() != '{') { + throw x.syntaxError("A JSONObject text must begin with '{'"); + } + for (; ; ) { + c = x.nextClean(); + switch (c) { + case 0: + throw x.syntaxError("A JSONObject text must end with '}'"); + case '}': + return; + default: + x.back(); + key = x.nextValue().toString(); + } + +// The key is followed by ':'. + + c = x.nextClean(); + if (c != ':') { + throw x.syntaxError("Expected a ':' after a key"); + } + this.putOnce(key, x.nextValue()); + +// Pairs are separated by ','. + + switch (x.nextClean()) { + case ';': + case ',': + if (x.nextClean() == '}') { + return; + } + x.back(); + break; + case '}': + return; + default: + throw x.syntaxError("Expected a ',' or '}'"); + } + } + } + + /** + * Construct a JSONObject from a Map. + * + * @param map A map object that can be used to initialize the contents of + * the JSONObject. + * @throws JSONException + */ + public JSONObject(Map map) { + this.map = new HashMap(); + if (map != null) { + Iterator> i = map.entrySet().iterator(); + while (i.hasNext()) { + Entry entry = i.next(); + Object value = entry.getValue(); + if (value != null) { + this.map.put(entry.getKey(), wrap(value)); + } + } + } + } + + /** + * Construct a JSONObject from an Object using bean getters. It reflects on + * all of the public methods of the object. For each of the methods with no + * parameters and a name starting with "get" or + * "is" followed by an uppercase letter, the method is invoked, + * and a key and the value returned from the getter method are put into the + * new JSONObject. + *

+ * The key is formed by removing the "get" or "is" + * prefix. If the second remaining character is not upper case, then the + * first character is converted to lower case. + *

+ * For example, if an object has a method named "getName", and + * if the result of calling object.getName() is + * "Larry Fine", then the JSONObject will contain + * "name": "Larry Fine". + * + * @param bean An object that has getter methods that should be used to make + * a JSONObject. + */ + public JSONObject(Object bean) { + this(); + this.populateMap(bean); + } + + /** + * Construct a JSONObject from an Object, using reflection to find the + * public members. The resulting JSONObject's keys will be the strings from + * the names array, and the values will be the field values associated with + * those keys in the object. If a key is not found or not visible, then it + * will not be copied into the new JSONObject. + * + * @param object An object that has fields that should be used to make a + * JSONObject. + * @param names An array of strings, the names of the fields to be obtained + * from the object. + */ + @SuppressWarnings("rawtypes") + public JSONObject(Object object, String names[]) { + this(); + Class c = object.getClass(); + for (int i = 0; i < names.length; i += 1) { + String name = names[i]; + try { + this.putOpt(name, c.getField(name).get(object)); + } catch (Exception ignore) { + } + } + } + + /** + * Construct a JSONObject from a source JSON text string. This is the most + * commonly used JSONObject constructor. + * + * @param source A string beginning with { (left + * brace) and ending with } + *  (right brace). + * @throws JSONException If there is a syntax error in the source string or a + * duplicated key. + */ + public JSONObject(String source) throws JSONException { + this(new JSONTokener(source)); + } + + /** + * Construct a JSONObject from a ResourceBundle. + * + * @param baseName The ResourceBundle base name. + * @param locale The Locale to load the ResourceBundle for. + * @throws JSONException If any JSONExceptions are detected. + */ + public JSONObject(String baseName, Locale locale) throws JSONException { + this(); + ResourceBundle bundle = ResourceBundle.getBundle(baseName, locale, + Thread.currentThread().getContextClassLoader()); + +// Iterate through the keys in the bundle. + + Enumeration keys = bundle.getKeys(); + while (keys.hasMoreElements()) { + Object key = keys.nextElement(); + if (key != null) { + +// Go through the path, ensuring that there is a nested JSONObject for each +// segment except the last. Add the value using the last segment's name into +// the deepest nested JSONObject. + + String[] path = ((String) key).split("\\."); + int last = path.length - 1; + JSONObject target = this; + for (int i = 0; i < last; i += 1) { + String segment = path[i]; + JSONObject nextTarget = target.optJSONObject(segment); + if (nextTarget == null) { + nextTarget = new JSONObject(); + target.put(segment, nextTarget); + } + target = nextTarget; + } + target.put(path[last], bundle.getString((String) key)); + } + } + } + + /** + * Accumulate values under a key. It is similar to the put method except + * that if there is already an object stored under the key then a JSONArray + * is stored under the key to hold all of the accumulated values. If there + * is already a JSONArray, then the new value is appended to it. In + * contrast, the put method replaces the previous value. + *

+ * If only one value is accumulated that is not a JSONArray, then the result + * will be the same as using put. But if multiple values are accumulated, + * then the result will be like append. + * + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the value is an invalid number or if the key is null. + */ + public JSONObject accumulate(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, + value instanceof JSONArray ? new JSONArray().put(value) + : value); + } else if (object instanceof JSONArray) { + ((JSONArray) object).put(value); + } else { + this.put(key, new JSONArray().put(object).put(value)); + } + return this; + } + + /** + * Append values to the array under a key. If the key does not exist in the + * JSONObject, then the key is put in the JSONObject with its value being a + * JSONArray containing the value parameter. If the key was already + * associated with a JSONArray, then the value parameter is appended to it. + * + * @param key A key string. + * @param value An object to be accumulated under the key. + * @return this. + * @throws JSONException If the key is null or if the current value associated with + * the key is not a JSONArray. + */ + public JSONObject append(String key, Object value) throws JSONException { + testValidity(value); + Object object = this.opt(key); + if (object == null) { + this.put(key, new JSONArray().put(value)); + } else if (object instanceof JSONArray) { + this.put(key, ((JSONArray) object).put(value)); + } else { + throw new JSONException("JSONObject[" + key + + "] is not a JSONArray."); + } + return this; + } + + /** + * Produce a string from a double. The string "null" will be returned if the + * number is not finite. + * + * @param d A double. + * @return A String. + */ + public static String doubleToString(double d) { + if (Double.isInfinite(d) || Double.isNaN(d)) { + return "null"; + } + +// Shave off trailing zeros and decimal point, if possible. + + String string = Double.toString(d); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get the value object associated with a key. + * + * @param key A key string. + * @return The object associated with the key. + * @throws JSONException if the key is not found. + */ + public Object get(String key) throws JSONException { + if (key == null) { + throw new JSONException("Null key."); + } + Object object = this.opt(key); + if (object == null) { + throw new JSONException("JSONObject[" + quote(key) + "] not found."); + } + return object; + } + + /** + * Get the boolean value associated with a key. + * + * @param key A key string. + * @return The truth. + * @throws JSONException if the value is not a Boolean or the String "true" or + * "false". + */ + public boolean getBoolean(String key) throws JSONException { + Object object = this.get(key); + if (object.equals(Boolean.FALSE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("false"))) { + return false; + } else if (object.equals(Boolean.TRUE) + || (object instanceof String && ((String) object) + .equalsIgnoreCase("true"))) { + return true; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a Boolean."); + } + + /** + * Get the double value associated with a key. + * + * @param key A key string. + * @return The numeric value. + * @throws JSONException if the key is not found or if the value is not a Number + * object and cannot be converted to a number. + */ + public double getDouble(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).doubleValue() + : Double.parseDouble((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a number."); + } + } + + /** + * Get the int value associated with a key. + * + * @param key A key string. + * @return The integer value. + * @throws JSONException if the key is not found or if the value cannot be converted + * to an integer. + */ + public int getInt(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).intValue() + : Integer.parseInt((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not an int."); + } + } + + /** + * Get the JSONArray value associated with a key. + * + * @param key A key string. + * @return A JSONArray which is the value. + * @throws JSONException if the key is not found or if the value is not a JSONArray. + */ + public JSONArray getJSONArray(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONArray) { + return (JSONArray) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONArray."); + } + + /** + * Get the JSONObject value associated with a key. + * + * @param key A key string. + * @return A JSONObject which is the value. + * @throws JSONException if the key is not found or if the value is not a JSONObject. + */ + public JSONObject getJSONObject(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof JSONObject) { + return (JSONObject) object; + } + throw new JSONException("JSONObject[" + quote(key) + + "] is not a JSONObject."); + } + + /** + * Get the long value associated with a key. + * + * @param key A key string. + * @return The long value. + * @throws JSONException if the key is not found or if the value cannot be converted + * to a long. + */ + public long getLong(String key) throws JSONException { + Object object = this.get(key); + try { + return object instanceof Number ? ((Number) object).longValue() + : Long.parseLong((String) object); + } catch (Exception e) { + throw new JSONException("JSONObject[" + quote(key) + + "] is not a long."); + } + } + + /** + * Get an array of field names from a JSONObject. + * + * @return An array of field names, or null if there are no names. + */ + public static String[] getNames(JSONObject jo) { + int length = jo.length(); + if (length == 0) { + return null; + } + Iterator iterator = jo.keys(); + String[] names = new String[length]; + int i = 0; + while (iterator.hasNext()) { + names[i] = iterator.next(); + i += 1; + } + return names; + } + + /** + * Get an array of field names from an Object. + * + * @return An array of field names, or null if there are no names. + */ + @SuppressWarnings("rawtypes") + public static String[] getNames(Object object) { + if (object == null) { + return null; + } + Class klass = object.getClass(); + Field[] fields = klass.getFields(); + int length = fields.length; + if (length == 0) { + return null; + } + String[] names = new String[length]; + for (int i = 0; i < length; i += 1) { + names[i] = fields[i].getName(); + } + return names; + } + + /** + * Get the string associated with a key. + * + * @param key A key string. + * @return A string which is the value. + * @throws JSONException if there is no string value for the key. + */ + public String getString(String key) throws JSONException { + Object object = this.get(key); + if (object instanceof String) { + return (String) object; + } + throw new JSONException("JSONObject[" + quote(key) + "] not a string."); + } + + /** + * Determine if the JSONObject contains a specific key. + * + * @param key A key string. + * @return true if the key exists in the JSONObject. + */ + public boolean has(String key) { + return this.map.containsKey(key); + } + + /** + * Increment a property of a JSONObject. If there is no such property, + * create one with a value of 1. If there is such a property, and if it is + * an Integer, Long, Double, or Float, then add one to it. + * + * @param key A key string. + * @return this. + * @throws JSONException If there is already a property with this name that is not an + * Integer, Long, Double, or Float. + */ + public JSONObject increment(String key) throws JSONException { + Object value = this.opt(key); + if (value == null) { + this.put(key, 1); + } else if (value instanceof Integer) { + this.put(key, (Integer) value + 1); + } else if (value instanceof Long) { + this.put(key, (Long) value + 1); + } else if (value instanceof Double) { + this.put(key, (Double) value + 1); + } else if (value instanceof Float) { + this.put(key, (Float) value + 1); + } else { + throw new JSONException("Unable to increment [" + quote(key) + "]."); + } + return this; + } + + /** + * Determine if the value associated with the key is null or if there is no + * value. + * + * @param key A key string. + * @return true if there is no value associated with the key or if the value + * is the JSONObject.NULL object. + */ + public boolean isNull(String key) { + return JSONObject.NULL.equals(this.opt(key)); + } + + /** + * Get an enumeration of the keys of the JSONObject. + * + * @return An iterator of the keys. + */ + public Iterator keys() { + return this.keySet().iterator(); + } + + /** + * Get a set of keys of the JSONObject. + * + * @return A keySet. + */ + public Set keySet() { + return this.map.keySet(); + } + + /** + * Get the number of keys stored in the JSONObject. + * + * @return The number of keys in the JSONObject. + */ + public int length() { + return this.map.size(); + } + + /** + * Produce a JSONArray containing the names of the elements of this + * JSONObject. + * + * @return A JSONArray containing the key strings, or null if the JSONObject + * is empty. + */ + public JSONArray names() { + JSONArray ja = new JSONArray(); + Iterator keys = this.keys(); + while (keys.hasNext()) { + ja.put(keys.next()); + } + return ja.length() == 0 ? null : ja; + } + + /** + * Produce a string from a Number. + * + * @param number A Number + * @return A String. + * @throws JSONException If n is a non-finite number. + */ + public static String numberToString(Number number) throws JSONException { + if (number == null) { + throw new JSONException("Null pointer"); + } + testValidity(number); + +// Shave off trailing zeros and decimal point, if possible. + + String string = number.toString(); + if (string.indexOf('.') > 0 && string.indexOf('e') < 0 + && string.indexOf('E') < 0) { + while (string.endsWith("0")) { + string = string.substring(0, string.length() - 1); + } + if (string.endsWith(".")) { + string = string.substring(0, string.length() - 1); + } + } + return string; + } + + /** + * Get an optional value associated with a key. + * + * @param key A key string. + * @return An object which is the value, or null if there is no value. + */ + public Object opt(String key) { + return key == null ? null : this.map.get(key); + } + + /** + * Get an optional boolean associated with a key. It returns false if there + * is no such key, or if the value is not Boolean.TRUE or the String "true". + * + * @param key A key string. + * @return The truth. + */ + public boolean optBoolean(String key) { + return this.optBoolean(key, false); + } + + /** + * Get an optional boolean associated with a key. It returns the + * defaultValue if there is no such key, or if it is not a Boolean or the + * String "true" or "false" (case insensitive). + * + * @param key A key string. + * @param defaultValue The default. + * @return The truth. + */ + public boolean optBoolean(String key, boolean defaultValue) { + try { + return this.getBoolean(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional double associated with a key, or NaN if there is no such + * key or if its value is not a number. If the value is a string, an attempt + * will be made to evaluate it as a number. + * + * @param key A string which is the key. + * @return An object which is the value. + */ + public double optDouble(String key) { + return this.optDouble(key, Double.NaN); + } + + /** + * Get an optional double associated with a key, or the defaultValue if + * there is no such key or if its value is not a number. If the value is a + * string, an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public double optDouble(String key, double defaultValue) { + try { + return this.getDouble(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional int value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public int optInt(String key) { + return this.optInt(key, 0); + } + + /** + * Get an optional int value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public int optInt(String key, int defaultValue) { + try { + return this.getInt(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional JSONArray associated with a key. It returns null if there + * is no such key, or if its value is not a JSONArray. + * + * @param key A key string. + * @return A JSONArray which is the value. + */ + public JSONArray optJSONArray(String key) { + Object o = this.opt(key); + return o instanceof JSONArray ? (JSONArray) o : null; + } + + /** + * Get an optional JSONObject associated with a key. It returns null if + * there is no such key, or if its value is not a JSONObject. + * + * @param key A key string. + * @return A JSONObject which is the value. + */ + public JSONObject optJSONObject(String key) { + Object object = this.opt(key); + return object instanceof JSONObject ? (JSONObject) object : null; + } + + /** + * Get an optional long value associated with a key, or zero if there is no + * such key or if the value is not a number. If the value is a string, an + * attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @return An object which is the value. + */ + public long optLong(String key) { + return this.optLong(key, 0); + } + + /** + * Get an optional long value associated with a key, or the default if there + * is no such key or if the value is not a number. If the value is a string, + * an attempt will be made to evaluate it as a number. + * + * @param key A key string. + * @param defaultValue The default. + * @return An object which is the value. + */ + public long optLong(String key, long defaultValue) { + try { + return this.getLong(key); + } catch (Exception e) { + return defaultValue; + } + } + + /** + * Get an optional string associated with a key. It returns an empty string + * if there is no such key. If the value is not a string and is not null, + * then it is converted to a string. + * + * @param key A key string. + * @return A string which is the value. + */ + public String optString(String key) { + return this.optString(key, ""); + } + + /** + * Get an optional string associated with a key. It returns the defaultValue + * if there is no such key. + * + * @param key A key string. + * @param defaultValue The default. + * @return A string which is the value. + */ + public String optString(String key, String defaultValue) { + Object object = this.opt(key); + return NULL.equals(object) ? defaultValue : object.toString(); + } + + @SuppressWarnings("rawtypes") + private void populateMap(Object bean) { + Class klass = bean.getClass(); + +// If klass is a System class then set includeSuperClass to false. + + boolean includeSuperClass = klass.getClassLoader() != null; + + Method[] methods = includeSuperClass ? klass.getMethods() : klass + .getDeclaredMethods(); + for (int i = 0; i < methods.length; i += 1) { + try { + Method method = methods[i]; + if (Modifier.isPublic(method.getModifiers())) { + String name = method.getName(); + String key = ""; + if (name.startsWith("get")) { + if ("getClass".equals(name) + || "getDeclaringClass".equals(name)) { + key = ""; + } else { + key = name.substring(3); + } + } else if (name.startsWith("is")) { + key = name.substring(2); + } + if (key.length() > 0 + && Character.isUpperCase(key.charAt(0)) + && method.getParameterTypes().length == 0) { + if (key.length() == 1) { + key = key.toLowerCase(); + } else if (!Character.isUpperCase(key.charAt(1))) { + key = key.substring(0, 1).toLowerCase() + + key.substring(1); + } + + Object result = method.invoke(bean, (Object[]) null); + if (result != null) { + this.map.put(key, wrap(result)); + } + } + } + } catch (Exception ignore) { + } + } + } + + /** + * Put a key/boolean pair in the JSONObject. + * + * @param key A key string. + * @param value A boolean which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, boolean value) throws JSONException { + this.put(key, value ? Boolean.TRUE : Boolean.FALSE); + return this; + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONArray which is produced from a Collection. + * + * @param key A key string. + * @param value A Collection value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Collection value) throws JSONException { + this.put(key, new JSONArray(value)); + return this; + } + + /** + * Put a key/double pair in the JSONObject. + * + * @param key A key string. + * @param value A double which is the value. + * @return this. + * @throws JSONException If the key is null or if the number is invalid. + */ + public JSONObject put(String key, double value) throws JSONException { + this.put(key, new Double(value)); + return this; + } + + /** + * Put a key/int pair in the JSONObject. + * + * @param key A key string. + * @param value An int which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, int value) throws JSONException { + this.put(key, new Integer(value)); + return this; + } + + /** + * Put a key/long pair in the JSONObject. + * + * @param key A key string. + * @param value A long which is the value. + * @return this. + * @throws JSONException If the key is null. + */ + public JSONObject put(String key, long value) throws JSONException { + this.put(key, new Long(value)); + return this; + } + + /** + * Put a key/value pair in the JSONObject, where the value will be a + * JSONObject which is produced from a Map. + * + * @param key A key string. + * @param value A Map value. + * @return this. + * @throws JSONException + */ + public JSONObject put(String key, Map value) throws JSONException { + this.put(key, new JSONObject(value)); + return this; + } + + /** + * Put a key/value pair in the JSONObject. If the value is null, then the + * key will be removed from the JSONObject if it is present. + * + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is non-finite number or if the key is null. + */ + public JSONObject put(String key, Object value) throws JSONException { + if (key == null) { + throw new NullPointerException("Null key."); + } + if (value != null) { + testValidity(value); + this.map.put(key, value); + } else { + this.remove(key); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null, and only if there is not already a member with that + * name. + * + * @param key string + * @param value object + * @return this. + * @throws JSONException if the key is a duplicate + */ + public JSONObject putOnce(String key, Object value) throws JSONException { + if (key != null && value != null) { + if (this.opt(key) != null) { + throw new JSONException("Duplicate key \"" + key + "\""); + } + this.put(key, value); + } + return this; + } + + /** + * Put a key/value pair in the JSONObject, but only if the key and the value + * are both non-null. + * + * @param key A key string. + * @param value An object which is the value. It should be of one of these + * types: Boolean, Double, Integer, JSONArray, JSONObject, Long, + * String, or the JSONObject.NULL object. + * @return this. + * @throws JSONException If the value is a non-finite number. + */ + public JSONObject putOpt(String key, Object value) throws JSONException { + if (key != null && value != null) { + this.put(key, value); + } + return this; + } + + /** + * Produce a string in double quotes with backslash sequences in all the + * right places. A backslash will be inserted within </, producing <\/, + * allowing JSON text to be delivered in HTML. In JSON text, a string cannot + * contain a control character or an unescaped quote or backslash. + * + * @param string A String + * @return A String correctly formatted for insertion in a JSON text. + */ + public static String quote(String string) { + StringWriter sw = new StringWriter(); + synchronized (sw.getBuffer()) { + try { + return quote(string, sw).toString(); + } catch (IOException ignored) { + // will never happen - we are writing to a string writer + return ""; + } + } + } + + public static Writer quote(String string, Writer w) throws IOException { + if (string == null || string.length() == 0) { + w.write("\"\""); + return w; + } + + char b; + char c = 0; + String hhhh; + int i; + int len = string.length(); + + w.write('"'); + for (i = 0; i < len; i += 1) { + b = c; + c = string.charAt(i); + switch (c) { + case '\\': + case '"': + w.write('\\'); + w.write(c); + break; + case '/': + if (b == '<') { + w.write('\\'); + } + w.write(c); + break; + case '\b': + w.write("\\b"); + break; + case '\t': + w.write("\\t"); + break; + case '\n': + w.write("\\n"); + break; + case '\f': + w.write("\\f"); + break; + case '\r': + w.write("\\r"); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + w.write("\\u"); + hhhh = Integer.toHexString(c); + w.write("0000", 0, 4 - hhhh.length()); + w.write(hhhh); + } else { + w.write(c); + } + } + } + w.write('"'); + return w; + } + + /** + * Remove a name and its value, if present. + * + * @param key The name to be removed. + * @return The value that was associated with the name, or null if there was + * no value. + */ + public Object remove(String key) { + return this.map.remove(key); + } + + /** + * Determine if two JSONObjects are similar. + * They must contain the same set of names which must be associated with + * similar values. + * + * @param other The other JSONObject + * @return true if they are equal + */ + public boolean similar(Object other) { + try { + if (!(other instanceof JSONObject)) { + return false; + } + Set set = this.keySet(); + if (!set.equals(((JSONObject) other).keySet())) { + return false; + } + Iterator iterator = set.iterator(); + while (iterator.hasNext()) { + String name = iterator.next(); + Object valueThis = this.get(name); + Object valueOther = ((JSONObject) other).get(name); + if (valueThis instanceof JSONObject) { + if (!((JSONObject) valueThis).similar(valueOther)) { + return false; + } + } else if (valueThis instanceof JSONArray) { + if (!((JSONArray) valueThis).similar(valueOther)) { + return false; + } + } else if (!valueThis.equals(valueOther)) { + return false; + } + } + return true; + } catch (Throwable exception) { + return false; + } + } + + /** + * Try to convert a string into a number, boolean, or null. If the string + * can't be converted, return the string. + * + * @param string A String. + * @return A simple JSON value. + */ + public static Object stringToValue(String string) { + Double d; + if (string.equals("")) { + return string; + } + if (string.equalsIgnoreCase("true")) { + return Boolean.TRUE; + } + if (string.equalsIgnoreCase("false")) { + return Boolean.FALSE; + } + if (string.equalsIgnoreCase("null")) { + return JSONObject.NULL; + } + + /* + * If it might be a number, try converting it. If a number cannot be + * produced, then the value will just be a string. + */ + + char b = string.charAt(0); + if ((b >= '0' && b <= '9') || b == '-') { + try { + if (string.indexOf('.') > -1 || string.indexOf('e') > -1 + || string.indexOf('E') > -1) { + d = Double.valueOf(string); + if (!d.isInfinite() && !d.isNaN()) { + return d; + } + } else { + Long myLong = new Long(string); + if (string.equals(myLong.toString())) { + if (myLong == myLong.intValue()) { + return myLong.intValue(); + } else { + return myLong; + } + } + } + } catch (Exception ignore) { + } + } + return string; + } + + /** + * Throw an exception if the object is a NaN or infinite number. + * + * @param o The object to test. + * @throws JSONException If o is a non-finite number. + */ + public static void testValidity(Object o) throws JSONException { + if (o != null) { + if (o instanceof Double) { + if (((Double) o).isInfinite() || ((Double) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } else if (o instanceof Float) { + if (((Float) o).isInfinite() || ((Float) o).isNaN()) { + throw new JSONException( + "JSON does not allow non-finite numbers."); + } + } + } + } + + /** + * Produce a JSONArray containing the values of the members of this + * JSONObject. + * + * @param names A JSONArray containing a list of key strings. This determines + * the sequence of the values in the result. + * @return A JSONArray of values. + * @throws JSONException If any of the values are non-finite numbers. + */ + public JSONArray toJSONArray(JSONArray names) throws JSONException { + if (names == null || names.length() == 0) { + return null; + } + JSONArray ja = new JSONArray(); + for (int i = 0; i < names.length(); i += 1) { + ja.put(this.opt(names.getString(i))); + } + return ja; + } + + /** + * Make a JSON text of this JSONObject. For compactness, no whitespace is + * added. If this would not result in a syntactically correct JSON text, + * then null will be returned instead. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + */ + public String toString() { + try { + return this.toString(0); + } catch (Exception e) { + return null; + } + } + + /** + * Make a prettyprinted JSON text of this JSONObject. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param indentFactor The number of spaces to add to each level of indentation. + * @return a printable, displayable, portable, transmittable representation + * of the object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException If the object contains an invalid number. + */ + public String toString(int indentFactor) throws JSONException { + StringWriter w = new StringWriter(); + synchronized (w.getBuffer()) { + return this.write(w, indentFactor, 0).toString(); + } + } + + /** + * Make a JSON text of an Object value. If the object has an + * value.toJSONString() method, then that method will be used to produce the + * JSON text. The method is required to produce a strictly conforming text. + * If the object does not contain a toJSONString method (which is the most + * common case), then a text will be produced by other means. If the value + * is an array or Collection, then a JSONArray will be made from it and its + * toJSONString method will be called. If the value is a MAP, then a + * JSONObject will be made from it and its toJSONString method will be + * called. Otherwise, the value's toString method will be called, and the + * result will be quoted. + *

+ *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @param value The value to be serialized. + * @return a printable, displayable, transmittable representation of the + * object, beginning with { (left + * brace) and ending with } (right + * brace). + * @throws JSONException If the value is or contains an invalid number. + */ + @SuppressWarnings("unchecked") + public static String valueToString(Object value) throws JSONException { + if (value == null || value.equals(null)) { + return "null"; + } + if (value instanceof JSONString) { + Object object; + try { + object = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + if (object instanceof String) { + return (String) object; + } + throw new JSONException("Bad value from toJSONString: " + object); + } + if (value instanceof Number) { + return numberToString((Number) value); + } + if (value instanceof Boolean || value instanceof JSONObject + || value instanceof JSONArray) { + return value.toString(); + } + if (value instanceof Map) { + return new JSONObject((Map) value).toString(); + } + if (value instanceof Collection) { + return new JSONArray((Collection) value).toString(); + } + if (value.getClass().isArray()) { + return new JSONArray(value).toString(); + } + return quote(value.toString()); + } + + /** + * Wrap an object, if necessary. If the object is null, return the NULL + * object. If it is an array or collection, wrap it in a JSONArray. If it is + * a map, wrap it in a JSONObject. If it is a standard property (Double, + * String, et al) then it is already wrapped. Otherwise, if it comes from + * one of the java packages, turn it into a string. And if it doesn't, try + * to wrap it in a JSONObject. If the wrapping fails, then null is returned. + * + * @param object The object to wrap + * @return The wrapped value + */ + @SuppressWarnings("unchecked") + public static Object wrap(Object object) { + try { + if (object == null) { + return NULL; + } + if (object instanceof JSONObject || object instanceof JSONArray + || NULL.equals(object) || object instanceof JSONString + || object instanceof Byte || object instanceof Character + || object instanceof Short || object instanceof Integer + || object instanceof Long || object instanceof Boolean + || object instanceof Float || object instanceof Double + || object instanceof String) { + return object; + } + + if (object instanceof Collection) { + return new JSONArray((Collection) object); + } + if (object.getClass().isArray()) { + return new JSONArray(object); + } + if (object instanceof Map) { + return new JSONObject((Map) object); + } + Package objectPackage = object.getClass().getPackage(); + String objectPackageName = objectPackage != null ? objectPackage + .getName() : ""; + if (objectPackageName.startsWith("java.") + || objectPackageName.startsWith("javax.") + || object.getClass().getClassLoader() == null) { + return object.toString(); + } + return new JSONObject(object); + } catch (Exception exception) { + return null; + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + public Writer write(Writer writer) throws JSONException { + return this.write(writer, 0, 0); + } + + @SuppressWarnings("unchecked") + static final Writer writeValue(Writer writer, Object value, + int indentFactor, int indent) throws JSONException, IOException { + if (value == null || value.equals(null)) { + writer.write("null"); + } else if (value instanceof JSONObject) { + ((JSONObject) value).write(writer, indentFactor, indent); + } else if (value instanceof JSONArray) { + ((JSONArray) value).write(writer, indentFactor, indent); + } else if (value instanceof Map) { + new JSONObject((Map) value).write(writer, indentFactor, indent); + } else if (value instanceof Collection) { + new JSONArray((Collection) value).write(writer, indentFactor, + indent); + } else if (value.getClass().isArray()) { + new JSONArray(value).write(writer, indentFactor, indent); + } else if (value instanceof Number) { + writer.write(numberToString((Number) value)); + } else if (value instanceof Boolean) { + writer.write(value.toString()); + } else if (value instanceof JSONString) { + Object o; + try { + o = ((JSONString) value).toJSONString(); + } catch (Exception e) { + throw new JSONException(e); + } + writer.write(o != null ? o.toString() : quote(value.toString())); + } else { + quote(value.toString(), writer); + } + return writer; + } + + static final void indent(Writer writer, int indent) throws IOException { + for (int i = 0; i < indent; i += 1) { + writer.write(' '); + } + } + + /** + * Write the contents of the JSONObject as JSON text to a writer. For + * compactness, no whitespace is added. + *

+ * Warning: This method assumes that the data structure is acyclical. + * + * @return The writer. + * @throws JSONException + */ + Writer write(Writer writer, int indentFactor, int indent) + throws JSONException { + try { + boolean commanate = false; + final int length = this.length(); + Iterator keys = this.keys(); + writer.write('{'); + + if (length == 1) { + Object key = keys.next(); + writer.write(quote(key.toString())); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + writeValue(writer, this.map.get(key), indentFactor, indent); + } else if (length != 0) { + final int newindent = indent + indentFactor; + while (keys.hasNext()) { + Object key = keys.next(); + if (commanate) { + writer.write(','); + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, newindent); + writer.write(quote(key.toString())); + writer.write(':'); + if (indentFactor > 0) { + writer.write(' '); + } + writeValue(writer, this.map.get(key), indentFactor, newindent); + commanate = true; + } + if (indentFactor > 0) { + writer.write('\n'); + } + indent(writer, indent); + } + writer.write('}'); + return writer; + } catch (IOException exception) { + throw new JSONException(exception); + } + } +} diff --git a/cloudinary-core/src/main/java/org/cloudinary/json/JSONString.java b/cloudinary-core/src/main/java/org/cloudinary/json/JSONString.java new file mode 100644 index 00000000..fc266601 --- /dev/null +++ b/cloudinary-core/src/main/java/org/cloudinary/json/JSONString.java @@ -0,0 +1,19 @@ +package org.cloudinary.json; + +/** + * The JSONString interface allows a toJSONString() + * method so that a class can change the behavior of + * JSONObject.toString(), JSONArray.toString(), + * and JSONWriter.value(Object). The + * toJSONString method will be used instead of the default behavior + * of using the Object's toString() method and quoting the result. + */ +public interface JSONString { + /** + * The toJSONString method allows a class to produce its own JSON + * serialization. + * + * @return A strictly syntactically correct JSON text. + */ + public String toJSONString(); +} diff --git a/cloudinary-core/src/main/java/org/cloudinary/json/JSONTokener.java b/cloudinary-core/src/main/java/org/cloudinary/json/JSONTokener.java new file mode 100644 index 00000000..ca1dcaaf --- /dev/null +++ b/cloudinary-core/src/main/java/org/cloudinary/json/JSONTokener.java @@ -0,0 +1,455 @@ +package org.cloudinary.json; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; + +/* +Copyright (c) 2002 JSON.org + +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 shall be used for Good, not Evil. + +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. +*/ + +/** + * A JSONTokener takes a source string and extracts characters and tokens from + * it. It is used by the JSONObject and JSONArray constructors to parse + * JSON source strings. + * + * @author JSON.org + * @version 2014-05-03 + */ +public class JSONTokener { + + private long character; + private boolean eof; + private long index; + private long line; + private char previous; + private Reader reader; + private boolean usePrevious; + + + /** + * Construct a JSONTokener from a Reader. + * + * @param reader A reader. + */ + public JSONTokener(Reader reader) { + this.reader = reader.markSupported() + ? reader + : new BufferedReader(reader); + this.eof = false; + this.usePrevious = false; + this.previous = 0; + this.index = 0; + this.character = 1; + this.line = 1; + } + + + /** + * Construct a JSONTokener from an InputStream. + * + * @param inputStream The source. + */ + public JSONTokener(InputStream inputStream) throws JSONException { + this(new InputStreamReader(inputStream)); + } + + + /** + * Construct a JSONTokener from a string. + * + * @param s A source string. + */ + public JSONTokener(String s) { + this(new StringReader(s)); + } + + + /** + * Back up one character. This provides a sort of lookahead capability, + * so that you can test for a digit or letter before attempting to parse + * the next number or identifier. + */ + public void back() throws JSONException { + if (this.usePrevious || this.index <= 0) { + throw new JSONException("Stepping back two steps is not supported"); + } + this.index -= 1; + this.character -= 1; + this.usePrevious = true; + this.eof = false; + } + + + /** + * Get the hex value of a character (base16). + * + * @param c A character between '0' and '9' or between 'A' and 'F' or + * between 'a' and 'f'. + * @return An int between 0 and 15, or -1 if c was not a hex digit. + */ + public static int dehexchar(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - ('A' - 10); + } + if (c >= 'a' && c <= 'f') { + return c - ('a' - 10); + } + return -1; + } + + public boolean end() { + return this.eof && !this.usePrevious; + } + + + /** + * Determine if the source string still contains characters that next() + * can consume. + * + * @return true if not yet at the end of the source. + */ + public boolean more() throws JSONException { + this.next(); + if (this.end()) { + return false; + } + this.back(); + return true; + } + + + /** + * Get the next character in the source string. + * + * @return The next character, or 0 if past the end of the source string. + */ + public char next() throws JSONException { + int c; + if (this.usePrevious) { + this.usePrevious = false; + c = this.previous; + } else { + try { + c = this.reader.read(); + } catch (IOException exception) { + throw new JSONException(exception); + } + + if (c <= 0) { // End of stream + this.eof = true; + c = 0; + } + } + this.index += 1; + if (this.previous == '\r') { + this.line += 1; + this.character = c == '\n' ? 0 : 1; + } else if (c == '\n') { + this.line += 1; + this.character = 0; + } else { + this.character += 1; + } + this.previous = (char) c; + return this.previous; + } + + + /** + * Consume the next character, and check that it matches a specified + * character. + * + * @param c The character to match. + * @return The character. + * @throws JSONException if the character does not match. + */ + public char next(char c) throws JSONException { + char n = this.next(); + if (n != c) { + throw this.syntaxError("Expected '" + c + "' and instead saw '" + + n + "'"); + } + return n; + } + + + /** + * Get the next n characters. + * + * @param n The number of characters to take. + * @return A string of n characters. + * @throws JSONException Substring bounds error if there are not + * n characters remaining in the source string. + */ + public String next(int n) throws JSONException { + if (n == 0) { + return ""; + } + + char[] chars = new char[n]; + int pos = 0; + + while (pos < n) { + chars[pos] = this.next(); + if (this.end()) { + throw this.syntaxError("Substring bounds error"); + } + pos += 1; + } + return new String(chars); + } + + + /** + * Get the next char in the string, skipping whitespace. + * + * @return A character, or 0 if there are no more characters. + * @throws JSONException + */ + public char nextClean() throws JSONException { + for (; ; ) { + char c = this.next(); + if (c == 0 || c > ' ') { + return c; + } + } + } + + + /** + * Return the characters up to the next close quote character. + * Backslash processing is done. The formal JSON format does not + * allow strings in single quotes, but an implementation is allowed to + * accept them. + * + * @param quote The quoting character, either + * " (double quote) or + * ' (single quote). + * @return A String. + * @throws JSONException Unterminated string. + */ + public String nextString(char quote) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (; ; ) { + c = this.next(); + switch (c) { + case 0: + case '\n': + case '\r': + throw this.syntaxError("Unterminated string"); + case '\\': + c = this.next(); + switch (c) { + case 'b': + sb.append('\b'); + break; + case 't': + sb.append('\t'); + break; + case 'n': + sb.append('\n'); + break; + case 'f': + sb.append('\f'); + break; + case 'r': + sb.append('\r'); + break; + case 'u': + sb.append((char) Integer.parseInt(this.next(4), 16)); + break; + case '"': + case '\'': + case '\\': + case '/': + sb.append(c); + break; + default: + throw this.syntaxError("Illegal escape."); + } + break; + default: + if (c == quote) { + return sb.toString(); + } + sb.append(c); + } + } + } + + + /** + * Get the text up but not including the specified character or the + * end of line, whichever comes first. + * + * @param delimiter A delimiter character. + * @return A string. + */ + public String nextTo(char delimiter) throws JSONException { + StringBuilder sb = new StringBuilder(); + for (; ; ) { + char c = this.next(); + if (c == delimiter || c == 0 || c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the text up but not including one of the specified delimiter + * characters or the end of line, whichever comes first. + * + * @param delimiters A set of delimiter characters. + * @return A string, trimmed. + */ + public String nextTo(String delimiters) throws JSONException { + char c; + StringBuilder sb = new StringBuilder(); + for (; ; ) { + c = this.next(); + if (delimiters.indexOf(c) >= 0 || c == 0 || + c == '\n' || c == '\r') { + if (c != 0) { + this.back(); + } + return sb.toString().trim(); + } + sb.append(c); + } + } + + + /** + * Get the next value. The value can be a Boolean, Double, Integer, + * JSONArray, JSONObject, Long, or String, or the JSONObject.NULL object. + * + * @return An object. + * @throws JSONException If syntax error. + */ + public Object nextValue() throws JSONException { + char c = this.nextClean(); + String string; + + switch (c) { + case '"': + case '\'': + return this.nextString(c); + case '{': + this.back(); + return new JSONObject(this); + case '[': + this.back(); + return new JSONArray(this); + } + + /* + * Handle unquoted text. This could be the values true, false, or + * null, or it can be a number. An implementation (such as this one) + * is allowed to also accept non-standard forms. + * + * Accumulate characters until we reach the end of the text or a + * formatting character. + */ + + StringBuilder sb = new StringBuilder(); + while (c >= ' ' && ",:]}/\\\"[{;=#".indexOf(c) < 0) { + sb.append(c); + c = this.next(); + } + this.back(); + + string = sb.toString().trim(); + if ("".equals(string)) { + throw this.syntaxError("Missing value"); + } + return JSONObject.stringToValue(string); + } + + + /** + * Skip characters until the next character is the requested character. + * If the requested character is not found, no characters are skipped. + * + * @param to A character to skip to. + * @return The requested character, or zero if the requested character + * is not found. + */ + public char skipTo(char to) throws JSONException { + char c; + try { + long startIndex = this.index; + long startCharacter = this.character; + long startLine = this.line; + this.reader.mark(1000000); + do { + c = this.next(); + if (c == 0) { + this.reader.reset(); + this.index = startIndex; + this.character = startCharacter; + this.line = startLine; + return c; + } + } while (c != to); + } catch (IOException exception) { + throw new JSONException(exception); + } + this.back(); + return c; + } + + + /** + * Make a JSONException to signal a syntax error. + * + * @param message The error message. + * @return A JSONException object, suitable for throwing + */ + public JSONException syntaxError(String message) { + return new JSONException(message + this.toString()); + } + + + /** + * Make a printable string of this JSONTokener. + * + * @return " at {index} [character {character} line {line}]" + */ + public String toString() { + return " at " + this.index + " [character " + this.character + " line " + + this.line + "]"; + } +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/AuthTokenTest.java b/cloudinary-core/src/test/java/com/cloudinary/AuthTokenTest.java new file mode 100644 index 00000000..49fd8d35 --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/AuthTokenTest.java @@ -0,0 +1,164 @@ +package com.cloudinary; + +import com.cloudinary.utils.ObjectUtils; + +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.Calendar; +import java.util.Collections; +import java.util.Map; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import static org.junit.Assert.*; + +public class AuthTokenTest { + public static final String KEY = "00112233FF99"; + public static final String ALT_KEY = "CCBB2233FF00"; + private Cloudinary cloudinary; + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary("cloudinary://a:b@test123?load_strategies=false&analytics=false"); + final AuthToken authToken = new AuthToken(KEY).duration(300); + authToken.startTime(11111111); // start time is set for test purposes + cloudinary.config.authToken = authToken; + cloudinary.config.cloudName = "test123"; + + } + + @Test + public void generateWithStartAndDuration() throws Exception { + AuthToken t = new AuthToken(KEY); + t.startTime(1111111111).acl("/image/*").duration(300); + assertEquals("should generate an authorization token with startTime and duration", "__cld_token__=st=1111111111~exp=1111111411~acl=%2fimage%2f*~hmac=1751370bcc6cfe9e03f30dd1a9722ba0f2cdca283fa3e6df3342a00a7528cc51", t.generate()); + } + + @Test + public void generateWithDuration() throws Exception { + long firstExp = Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L + 300; + Thread.sleep(1200); + String token = new AuthToken(KEY).acl("*").duration(300).generate(); + Thread.sleep(1200); + long secondExp = Calendar.getInstance(TimeZone.getTimeZone("UTC")).getTimeInMillis() / 1000L + 300; + Matcher m = Pattern.compile("exp=(\\d+)").matcher(token); + assertTrue(m.find()); + final String expString = m.group(1); + final long actual = Long.parseLong(expString); + assertThat(actual, Matchers.greaterThanOrEqualTo(firstExp)); + assertThat(actual, Matchers.lessThanOrEqualTo(secondExp)); + assertEquals(token, new AuthToken(KEY).acl("*").expiration(actual).generate()); + } + + @Test(expected = IllegalArgumentException.class) + public void testMustProvideExpirationOrDuration(){ + new AuthToken(KEY).acl("*").generate(); + } + + @Test + public void testAuthenticatedUrl() { + cloudinary.config.privateCdn = true; + + String message = "should add token if authToken is globally set and signed = true"; + String url = cloudinary.url().signed(true).resourceType("image").type("authenticated").version("1486020273").generate("sample.jpg"); + assertEquals(message,"https://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3", url); + + message = "should add token for 'public' resource"; + url = cloudinary.url().signed(true).resourceType("image").type("public").version("1486020273").generate("sample.jpg"); + assertEquals(message,"https://test123-res.cloudinary.com/image/public/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=c2b77d9f81be6d89b5d0ebc67b671557e88a40bcf03dd4a6997ff4b994ceb80e", url); + + message = "should not add token if signed is false"; + url = cloudinary.url().resourceType("image").type("authenticated").version("1486020273").generate("sample.jpg"); + assertEquals(message,"https://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg", url); + + message = "should not add token if authToken is globally set but null auth token is explicitly set and signed = true"; + url = cloudinary.url().authToken(AuthToken.NULL_AUTH_TOKEN).signed(true).resourceType("image").type("authenticated").version("1486020273").generate("sample.jpg"); + assertEquals(message,"https://test123-res.cloudinary.com/image/authenticated/s--v2fTPYTu--/v1486020273/sample.jpg", url); + + message = "explicit authToken should override global setting"; + url = cloudinary.url().signed(true).authToken(new AuthToken(ALT_KEY).startTime(222222222).duration(100)).resourceType("image").type("authenticated").transformation(new Transformation().crop("scale").width(300)).generate("sample.jpg"); + assertEquals(message,"https://test123-res.cloudinary.com/image/authenticated/c_scale,w_300/sample.jpg?__cld_token__=st=222222222~exp=222222322~hmac=55cfe516530461213fe3b3606014533b1eca8ff60aeab79d1bb84c9322eebc1f", url); + + message = "should compute expiration as start time + duration"; + url = cloudinary.url().signed(true).authToken(new AuthToken().startTime(11111111).duration(300)) + .type("authenticated").version("1486020273").generate("sample.jpg"); + assertEquals(message,"https://test123-res.cloudinary.com/image/authenticated/v1486020273/sample.jpg?__cld_token__=st=11111111~exp=11111411~hmac=8db0d753ee7bbb9e2eaf8698ca3797436ba4c20e31f44527e43b6a6e995cfdb3", url); + + } + + @Test + public void testConfiguration() { + cloudinary = new Cloudinary("cloudinary://a:b@test123?load_strategies=false&auth_token[key]=aabbcc112233&auth_token[duration]=200"); + assertEquals(cloudinary.config.authToken, new AuthToken("aabbcc112233").duration(200)); + } + + @Test + public void testTokenGeneration(){ + AuthToken token = new AuthToken(KEY); + token.duration(300); + String user = "foobar"; // username taken from elsewhere + token.acl("/*/t_" + user); + token.startTime(222222222); // we can't rely on the default "now" value in tests + String cookieToken = token.generate(); + assertEquals("__cld_token__=st=222222222~exp=222222522~acl=%2f*%2ft_foobar~hmac=8e39600cc18cec339b21fe2b05fcb64b98de373355f8ce732c35710d8b10259f", cookieToken); + } + + @Test + public void testUrlInTag() { + String message = "should add token to an image tag url"; + String url = cloudinary.url().signed(true).resourceType("image").type("authenticated").version("1486020273").imageTag("sample.jpg"); + assertThat(url, Matchers.matchesPattern("")); + + } + + @Test + public void testIgnoreUrlIfAclIsProvided() { + String user = "foobar"; // username taken from elsewhere + AuthToken token = new AuthToken(KEY).duration(300).acl("/*/t_" + user).startTime(222222222); + String cookieToken = token.generate(); + AuthToken aclToken = new AuthToken(KEY).duration(300).acl("/*/t_" + user).startTime(222222222); + String cookieAclToken = aclToken.generate("http://res.cloudinary.com/test123/image/upload/v1486020273/sample.jpg"); + assertEquals(cookieToken, cookieAclToken); + } + + @Test + public void testMultiplePatternsInAcl() { + AuthToken token = new AuthToken(KEY).duration(3600).acl("/image/authenticated/*", "/image2/authenticated/*", "/image3/authenticated/*").startTime(22222222); + String cookieToken = token.generate(); + Assert.assertThat(cookieToken, CoreMatchers.containsString("~acl=%2fimage%2fauthenticated%2f*!%2fimage2%2fauthenticated%2f*!%2fimage3%2fauthenticated%2f*~")); + } + + @Test + public void testPublicAclInitializationFromMap() { + Map options = ObjectUtils.asMap( + "acl", Collections.singleton("foo"), + "expiration", 100, + "key", KEY, + "tokenName", "token"); + String token = new AuthToken(options).generate(); + assertEquals("token=exp=100~acl=foo~hmac=88be250f3a912add862959076ee74f392fa0959a953fddd9128787d5c849efd9", token); + } + + @Test(expected = IllegalArgumentException.class) + public void testMissingAclAndUrlShouldThrow() { + String token = new AuthToken(KEY).duration(300).generate(); + } + + @Test + public void testMissingUrlNotMissingAclShouldNotThrow() { + String token = new AuthToken(KEY).duration(300).generate("http://res.cloudinary.com/test123"); + } + + +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/TransformationTest.java b/cloudinary-core/src/test/java/com/cloudinary/TransformationTest.java new file mode 100644 index 00000000..1c7f93ba --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/TransformationTest.java @@ -0,0 +1,381 @@ +package com.cloudinary; + +import com.cloudinary.transformation.Condition; +import com.cloudinary.transformation.TextLayer; +import com.cloudinary.utils.ObjectUtils; +import org.cloudinary.json.JSONArray; +import org.hamcrest.CoreMatchers; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; + +import java.util.*; + +import static com.cloudinary.transformation.Expression.faceCount; +import static com.cloudinary.transformation.Expression.variable; +import static org.junit.Assert.*; + +/** + * + */ +@SuppressWarnings("unchecked") +@RunWith(JUnitParamsRunner.class) +public class TransformationTest { + + @Before + public void setUp() throws Exception { + + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void withLiteral() throws Exception { + Transformation transformation = new Transformation().ifCondition("w_lt_200").crop("fill").height(120).width(80); + assertEquals("should include the if parameter as the first component in the transformation string", "if_w_lt_200,c_fill,h_120,w_80", transformation.toString()); + + transformation = new Transformation().crop("fill").height(120).ifCondition("w_lt_200").width(80); + assertEquals("should include the if parameter as the first component in the transformation string", "if_w_lt_200,c_fill,h_120,w_80", transformation.toString()); + + String chained = "[{if: \"w_lt_200\",crop: \"fill\",height: 120, width: 80}, {if: \"w_gt_400\",crop: \"fit\",width: 150,height: 150},{effect: \"sepia\"}]"; + List transformations = ObjectUtils.toList(new JSONArray(chained)); + + transformation = new Transformation(transformations); + assertEquals("should allow multiple conditions when chaining transformations", "if_w_lt_200,c_fill,h_120,w_80/if_w_gt_400,c_fit,h_150,w_150/e_sepia", transformation.toString()); + } + + @Test + public void literalWithSpaces() throws Exception { + Map map = ObjectUtils.asMap("if", "width < 200", "crop", "fill", "height", 120, "width", 80); + List list = new ArrayList(); + list.add(map); + Transformation transformation = new Transformation(list); + + assertEquals("should translate operators", "if_w_lt_200,c_fill,h_120,w_80", transformation.toString()); + } + + @Test + public void endIf() throws Exception { + String chained = "[{if: \"w_lt_200\"},\n" + + " {crop: \"fill\", height: 120, width: 80,effect: \"sharpen\"},\n" + + " {effect: \"brightness:50\"},\n" + + " {effect: \"shadow\",color: \"red\"}, {if: \"end\"}]"; + List transformations = ObjectUtils.toList(new JSONArray(chained)); + + Transformation transformation = new Transformation(transformations); + assertEquals("should include the if_end as the last parameter in its component", "if_w_lt_200/c_fill,e_sharpen,h_120,w_80/e_brightness:50/co_red,e_shadow/if_end", transformation.toString()); + + } + + @Test + public void ifElse() throws Exception { + String chained = "[{if: \"w_lt_200\",crop: \"fill\",height: 120,width: 80},\n" + + " {if: \"else\",crop: \"fill\",height: 90, width: 100}]"; + List transformations = ObjectUtils.toList(new JSONArray(chained)); + + Transformation transformation = new Transformation(transformations); + + assertEquals("should support if_else with transformation parameters", "if_w_lt_200,c_fill,h_120,w_80/if_else,c_fill,h_90,w_100", transformation.toString()); + + chained = "[{if: \"w_lt_200\"},\n" + + " {crop: \"fill\",height: 120,width: 80},\n" + + " {if: \"else\"},\n" + + " {crop: \"fill\",height: 90,width: 100}]"; + transformations = ObjectUtils.toList(new JSONArray(chained)); + + transformation = new Transformation(transformations); + assertEquals("if_else should be without any transformation parameters", "if_w_lt_200/c_fill,h_120,w_80/if_else/c_fill,h_90,w_100", transformation.toString()); + } + + @Test + public void testDuration() throws Exception { + Transformation transformation = new Transformation().ifCondition().duration("gt", "30").then().width(100).crop("scale"); + assertEquals("passing an operator and a value adds a condition", "if_du_gt_30,c_scale,w_100", transformation.toString()); + transformation = new Transformation().ifCondition().initialDuration("gt", "30").then().width(100).crop("scale"); + assertEquals("passing an operator and a value adds a condition", "if_idu_gt_30,c_scale,w_100", transformation.toString()); + transformation=new Transformation().ifCondition("initialDuration > 20").crop("scale").width(200); + assertEquals("if_idu_gt_20,c_scale,w_200", transformation.generate()); + } + + + @Test + public void chainedConditions() throws Exception { + Transformation transformation = new Transformation().ifCondition().aspectRatio("gt", "3:4").then().width(100).crop("scale"); + assertEquals("passing an operator and a value adds a condition", "if_ar_gt_3:4,c_scale,w_100", transformation.toString()); + transformation = new Transformation().ifCondition().aspectRatio("gt", "3:4").and().width("gt", 100).then().width(50).crop("scale"); + assertEquals("should chaining condition with `and`", "if_ar_gt_3:4_and_w_gt_100,c_scale,w_50", transformation.toString()); + transformation = new Transformation().ifCondition().aspectRatio("gt", "3:4").and().width("gt", 100).or().width("gt", 200).then().width(50).crop("scale"); + assertEquals("should chain conditions with `or`", "if_ar_gt_3:4_and_w_gt_100_or_w_gt_200,c_scale,w_50", transformation.toString()); + transformation = new Transformation().ifCondition().aspectRatio(">", "3:4").and().width("<=", 100).or().width("gt", 200).then().width(50).crop("scale"); + assertEquals("should translate operators", "if_ar_gt_3:4_and_w_lte_100_or_w_gt_200,c_scale,w_50", transformation.toString()); + transformation = new Transformation().ifCondition().aspectRatio(">", "3:4").and().width("<=", 100).or().width(">", 200).then().width(50).crop("scale"); + assertEquals("should translate operators", "if_ar_gt_3:4_and_w_lte_100_or_w_gt_200,c_scale,w_50", transformation.toString()); + transformation = new Transformation().ifCondition().aspectRatio(">=", "3:4").and().pageCount(">=", 100).or().pageCount("!=", 0).then().width(50).crop("scale"); + assertEquals("should translate operators", "if_ar_gte_3:4_and_pc_gte_100_or_pc_ne_0,c_scale,w_50", transformation.toString()); + + } + + @Test + public void shouldSupportAndTranslateOperators() { + + String allOperators = + "if_" + + "w_eq_0_and" + + "_h_ne_0_or" + + "_ar_lt_0_and" + + "_pc_gt_0_and" + + "_fc_lte_0_and" + + "_w_gte_0" + + ",e_grayscale"; + assertEquals("should support and translate operators: '=', '!=', '<', '>', '<=', '>=', '&&', '||'", + allOperators, new Transformation().ifCondition() + .width("=", 0).and() + .height("!=", 0).or() + .aspectRatio("<", 0).and() + .pageCount(">", 0).and() + .faceCount("<=", 0).and() + .width(">=", 0) + .then().effect("grayscale").toString()); + + assertEquals(allOperators, new Transformation().ifCondition("w = 0 && height != 0 || aspectRatio < 0 and pageCount > 0 and faceCount <= 0 and width >= 0") + .effect("grayscale") + .toString()); + } + + @Test + public void endIf2() throws Exception { + Transformation transformation = new Transformation().ifCondition().width("gt", 100).and().width("lt", 200).then().width(50).crop("scale").endIf(); + assertEquals("should serialize to 'if_end'", "if_w_gt_100_and_w_lt_200/c_scale,w_50/if_end", transformation.toString()); + transformation = new Transformation().ifCondition().width("gt", 100).and().width("lt", 200).then().width(50).crop("scale").endIf(); + assertEquals("force the if clause to be chained", "if_w_gt_100_and_w_lt_200/c_scale,w_50/if_end", transformation.toString()); + transformation = new Transformation().ifCondition().width("gt", 100).and().width("lt", 200).then().width(50).crop("scale").ifElse().width(100).crop("crop").endIf(); + assertEquals("force the if_else clause to be chained", "if_w_gt_100_and_w_lt_200/c_scale,w_50/if_else/c_crop,w_100/if_end", transformation.toString()); + + } + + @Test + public void testArrayShouldDefineASetOfVariables() { + // using methods + Transformation t = new Transformation(); + t.ifCondition("face_count > 2") + .variables(variable("$z", 5), variable("$foo", "$z * 2")) + .crop("scale") + .width("$foo * 200"); + assertEquals("if_fc_gt_2,$z_5,$foo_$z_mul_2,c_scale,w_$foo_mul_200", t.generate()); + } + + @Test + public void testShouldSortDefinedVariable(){ + Transformation t = new Transformation().variable("$second", 1).variable("$first", 2); + assertEquals("$first_2,$second_1", t.generate()); + } + + @Test + public void testShouldPlaceDefinedVariablesBeforeOrdered(){ + Transformation t = new Transformation() + .variables(variable("$z", 5), variable("$foo", "$z * 2")) + .variable("$second", 1) + .variable("$first", 2); + assertEquals("$first_2,$second_1,$z_5,$foo_$z_mul_2", t.generate()); + } + + @Test + public void testVariable(){ + // using strings + Transformation t = new Transformation(); + t.variable("$foo", 10) + .chain() + .ifCondition(faceCount().gt(2)) + .crop("scale") + .width(new Condition("$foo * 200 / faceCount")) + .endIf(); + assertEquals("$foo_10/if_fc_gt_2/c_scale,w_$foo_mul_200_div_fc/if_end", t.generate()); + } + + @Test + public void testShouldSupportTextValues() { + Transformation t = new Transformation(); + t.effect("$efname", 100) + .variable("$efname", "!blur!"); + assertEquals("$efname_!blur!,e_$efname:100", t.generate()); + } + + @Test + public void testSupportStringInterpolation() { + Transformation t = new Transformation() + .crop("scale") + .overlay(new TextLayer().text( + "$(start)Hello $(name)$(ext), $(no ) $( no)$(end)" + ).fontFamily("Arial").fontSize(18)); + assertEquals("c_scale,l_text:Arial_18:$(start)Hello%20$(name)$(ext)%252C%20%24%28no%20%29%20%24%28%20no%29$(end)", t.generate()); + } + + @Test + public void testShouldSupportPowOperator() { + Transformation t = new Transformation() + .variables(variable("$small", 150), variable("$big", "$small ^ 1.5")); + + assertEquals("$small_150,$big_$small_pow_1.5", t.generate()); + } + + @Test + public void testShouldNotChangeVariableNamesWhenTheyNamedAfterKeyword() { + Transformation t = new Transformation() + .variable("$width", 10) + .chain() + .width("$width + 10 + width"); + + assertEquals("$width_10/w_$width_add_10_add_w", t.generate()); + } + + @Test + public void testRadiusTwoCornersAsValues() { + Transformation t = new Transformation() + .radius(10, 20); + + assertEquals("r_10:20", t.generate()); + } + + @Test + public void testRadiusTwoCornersAsExpressions() { + Transformation t = new Transformation() + .radius("10", "$v"); + + assertEquals("r_10:$v", t.generate()); + } + + @Test + public void testRadiusThreeCorners() { + Transformation t = new Transformation() + .radius(10, "$v", "30"); + + assertEquals("r_10:$v:30", t.generate()); + } + + @Test + public void testRadiusFourCorners() { + Transformation t = new Transformation() + .radius(10, "$v", "30", 40); + + assertEquals("r_10:$v:30:40", t.generate()); + } + + @Test + public void testRadiusArray1() { + Transformation t = new Transformation() + .radius(new Object[]{10}); + + assertEquals("r_10", t.generate()); + } + + @Test + public void testRadiusArray2() { + Transformation t = new Transformation() + .radius(new Object[]{10, "$v"}); + + assertEquals("r_10:$v", t.generate()); + } + + @Test + public void testUserVariableNamesContainingPredefinedNamesAreNotAffected() { + Transformation t = new Transformation() + .variable("$mywidth", "100") + .variable("$aheight", 300) + .chain() + .width("3 + $mywidth * 3 + 4 / 2 * initialWidth * $mywidth") + .height("3 * initialHeight + $aheight"); + + assertEquals("$aheight_300,$mywidth_100/h_3_mul_ih_add_$aheight,w_3_add_$mywidth_mul_3_add_4_div_2_mul_iw_mul_$mywidth", t.generate()); + } + + @Test + public void testContextMetadataToUserVariables() { + Transformation t = new Transformation() + .variable("$xpos", "ctx:!x_pos!_to_f") + .variable("$ypos", "ctx:!y_pos!_to_f") + .crop("crop") + .x("$xpos * w") + .y("$ypos * h"); + + assertEquals("$xpos_ctx:!x_pos!_to_f,$ypos_ctx:!y_pos!_to_f,c_crop,x_$xpos_mul_w,y_$ypos_mul_h", t.generate()); + } + + @Test + public void testFormatInTransformation() { + String t = new EagerTransformation().width(100).format("jpeg").generate(); + assertEquals("w_100/jpeg", t); + + t = new EagerTransformation().width(100).format("").generate(); + assertEquals("w_100/", t); + } + + @Parameters({ "angle", + "aspect_ratio", + "dpr", + "effect", + "height", + "opacity", + "quality", + "width", + "x", + "y", + "end_offset", + "start_offset", + "zoom" }) + @Test + public void testVerifyNormalizationShouldNormalize(String input) throws Exception { + String t = new Transformation().param(input, "width * 2").generate(); + assertThat(t, CoreMatchers.containsString("w_mul_2")); + } + + @Parameters({ + "audio_codec", + "audio_frequency", + "border", + "bit_rate", + "color_space", + "default_image", + "delay", + "density", + "fetch_format", + "custom_function", + "fps", + "gravity", + "overlay", + "prefix", + "page", + "underlay", + "video_sampling", + "streaming_profile", + "keyframe_interval"}) + @Test + public void testVerifyNormalizationShouldNotNormalize(String input) throws Exception { + String t = new Transformation().param(input, "width * 2").generate(); + assertThat(t, CoreMatchers.not(CoreMatchers.containsString("w_mul_2"))); + } + + @Test + public void testSupportStartOffset() throws Exception { + String t = new Transformation().width(100).startOffset("idu - 5").generate(); + assertThat(t, CoreMatchers.containsString("so_idu_sub_5")); + + t = new Transformation().width(100).startOffset("$logotime").generate(); + assertThat(t, CoreMatchers.containsString("so_$logotime")); + } + + @Test + public void testSupportEndOffset() throws Exception { + String t = new Transformation().width(100).endOffset("idu - 5").generate(); + assertThat(t, CoreMatchers.containsString("eo_idu_sub_5")); + + t = new Transformation().width(100).endOffset("$logotime").generate(); + assertThat(t, CoreMatchers.containsString("eo_$logotime")); + } +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/UtilTest.java b/cloudinary-core/src/test/java/com/cloudinary/UtilTest.java new file mode 100644 index 00000000..6794e277 --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/UtilTest.java @@ -0,0 +1,162 @@ +package com.cloudinary; + +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.cloudinary.json.JSONObject; +import org.junit.Test; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Map; + +import static org.junit.Assert.*; + +/** + * Created by amir on 17/11/2016. + */ +public class UtilTest { + + public static final String START = "2019-02-22 16:20:57 +0200"; + public static final String END = "2019-03-22 00:00:00 +0200"; + public static final String START_REFORMATTED = "2019-02-22T14:20:57Z"; + public static final String END_REFORMATTED = "2019-03-21T22:00:00Z"; + + @Test + public void encodeContext() throws Exception { + Map context = ObjectUtils.asMap("caption", "different = caption", "alt2", "alt|alternative"); + String result = Util.encodeContext(context); + assertTrue("caption=different \\= caption|alt2=alt\\|alternative".equals(result) || + "alt2=alt\\|alternative|caption=different \\= caption".equals(result)); + } + + @Test + public void testAccessControlRule() throws ParseException { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + final Date start = simpleDateFormat.parse(START); + final Date end = simpleDateFormat.parse(END); + AccessControlRule acl = AccessControlRule.anonymous(start, end); + + JSONObject deserializedAcl = new JSONObject(acl.toString()); + assertEquals(deserializedAcl.get("access_type"), "anonymous"); + assertEquals(deserializedAcl.get("start"), START_REFORMATTED); + assertEquals(deserializedAcl.get("end"), END_REFORMATTED); + + acl = AccessControlRule.anonymousFrom(start); + assertEquals(2, acl.length()); + assertEquals(deserializedAcl.get("access_type"), "anonymous"); + assertEquals(deserializedAcl.get("start"), START_REFORMATTED); + + acl = AccessControlRule.anonymousUntil(end); + assertEquals(2, acl.length()); + assertEquals(deserializedAcl.get("access_type"), "anonymous"); + assertEquals(deserializedAcl.get("end"), END_REFORMATTED); + + AccessControlRule token = AccessControlRule.token(); + assertEquals(1, token.length()); + assertEquals("{\"access_type\":\"token\"}", token.toString()); + + } + + @Test + public void testMergeToSingleUnderscore() { + assertEquals("a_b_c_d", StringUtils.mergeToSingleUnderscore("a_b_c_d")); + assertEquals("a_b_c_d", StringUtils.mergeToSingleUnderscore("a_b_c_ d")); + assertEquals("a_b_c_d", StringUtils.mergeToSingleUnderscore("a_b_c_ _ d")); + assertEquals("_a_b_c_d_", StringUtils.mergeToSingleUnderscore("___ _ a____ b_c_ d _ _")); + assertEquals("a", StringUtils.mergeToSingleUnderscore("a")); + assertEquals("a_", StringUtils.mergeToSingleUnderscore("a___________")); + assertEquals("_a", StringUtils.mergeToSingleUnderscore(" a")); + } + + @Test + public void testIsVariable(){ + assertTrue(StringUtils.isVariable("$a6")); + assertTrue(StringUtils.isVariable("$a64534534")); + assertTrue(StringUtils.isVariable("$ab")); + assertTrue(StringUtils.isVariable("$asdasda")); + assertTrue(StringUtils.isVariable("$a34asd12e")); + assertTrue(StringUtils.isVariable("$a")); + + assertFalse(StringUtils.isVariable("sda")); + assertFalse(StringUtils.isVariable(" ")); + assertFalse(StringUtils.isVariable("... . /")); + assertFalse(StringUtils.isVariable("$")); + assertFalse(StringUtils.isVariable("$4")); + assertFalse(StringUtils.isVariable("$4dfds")); + assertFalse(StringUtils.isVariable("$612s")); + assertFalse(StringUtils.isVariable("$6 12s")); + assertFalse(StringUtils.isVariable("$6 1.2s")); + } + + @Test + public void testReplaceIfFirstChar(){ + assertEquals("abcdef", StringUtils.replaceIfFirstChar("abcdef", 'b', "*")); + assertEquals("abcdef", StringUtils.replaceIfFirstChar("abcdef", 'f', "*")); + assertEquals("abcdef", StringUtils.replaceIfFirstChar("abcdef", 'z', "*")); + assertEquals("abcdef", StringUtils.replaceIfFirstChar("abcdef", '4', "*")); + assertEquals("abcdef", StringUtils.replaceIfFirstChar("abcdef", '$', "*")); + assertEquals("abc#def", StringUtils.replaceIfFirstChar("abc#def", 'b', "*")); + assertEquals("$%^bcdef", StringUtils.replaceIfFirstChar("$%^bcdef", 'b', "*")); + + assertEquals("*bcdef", StringUtils.replaceIfFirstChar("abcdef", 'a', "*")); + assertEquals("***bcdef", StringUtils.replaceIfFirstChar("abcdef", 'a', "***")); + assertEquals("aaabcdef", StringUtils.replaceIfFirstChar("abcdef", 'a', "aaa")); + assertEquals("---%^bcdef", StringUtils.replaceIfFirstChar("$%^bcdef", '$', "---")); + + } + + @Test + public void testIsHttpUrl(){ + assertTrue(StringUtils.isHttpUrl("http://earsadasdsad")); + assertTrue(StringUtils.isHttpUrl("https://earsadasdsad")); + assertTrue(StringUtils.isHttpUrl("http://")); + assertTrue(StringUtils.isHttpUrl("https://")); + + assertFalse(StringUtils.isHttpUrl("dafadfasd")); + assertFalse(StringUtils.isHttpUrl("dafadfasd#$@")); + assertFalse(StringUtils.isHttpUrl("htt://")); + } + + @Test + public void testMergeSlashes(){ + assertEquals("a/b/c/d/e", StringUtils.mergeSlashesInUrl("a////b///c//d/e")); + assertEquals("abcd",StringUtils.mergeSlashesInUrl( "abcd")); + assertEquals("ab/cd",StringUtils.mergeSlashesInUrl( "ab/cd")); + assertEquals("/abcd",StringUtils.mergeSlashesInUrl( "/////abcd")); + assertEquals("/abcd/",StringUtils.mergeSlashesInUrl( "////abcd///")); + assertEquals("/abcd/",StringUtils.mergeSlashesInUrl( "/abcd/")); + } + + @Test + public void testStartWithVersionString(){ + assertTrue(StringUtils.startWithVersionString("v1")); + assertTrue(StringUtils.startWithVersionString("v1fdasfasd")); + assertTrue(StringUtils.startWithVersionString("v112fdasfasd")); + assertTrue(StringUtils.startWithVersionString("v112/fda/sfasd")); + assertTrue(StringUtils.startWithVersionString("v112/fda/v1sfasd")); + assertTrue(StringUtils.startWithVersionString("/v112/fda/v1sfasd")); + + assertFalse(StringUtils.startWithVersionString("asdasv1fdasfasd")); + assertFalse(StringUtils.startWithVersionString("12v1fdasfasd")); + assertFalse(StringUtils.startWithVersionString("asdasv1fdasfasd")); + assertFalse(StringUtils.startWithVersionString("wqeasdlv31423423")); + assertFalse(StringUtils.startWithVersionString("wqeasdl/v31423423")); + assertFalse(StringUtils.startWithVersionString("121fdasfasd")); + assertFalse(StringUtils.startWithVersionString("/121fdasfasd")); + assertFalse(StringUtils.startWithVersionString("/")); + assertFalse(StringUtils.startWithVersionString("/v")); + assertFalse(StringUtils.startWithVersionString("")); + assertFalse(StringUtils.startWithVersionString("vvv")); + assertFalse(StringUtils.startWithVersionString("v")); + assertFalse(StringUtils.startWithVersionString("asdvvv")); + } + + @Test + public void testRemoveStartingChars(){ + assertEquals("abcde", StringUtils.removeStartingChars("abcde", 'b')); + assertEquals("bcde", StringUtils.removeStartingChars("abcde", 'a')); + assertEquals("bcde", StringUtils.removeStartingChars("aaaaaabcde", 'a')); + assertEquals("bcdeaa", StringUtils.removeStartingChars("aaaaaabcdeaa", 'a')); + } +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/analytics/AnalyticsTest.java b/cloudinary-core/src/test/java/com/cloudinary/analytics/AnalyticsTest.java new file mode 100644 index 00000000..e87143c6 --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/analytics/AnalyticsTest.java @@ -0,0 +1,146 @@ +package com.cloudinary.analytics; + +import com.cloudinary.AuthToken; +import com.cloudinary.Cloudinary; +import com.cloudinary.utils.Analytics; +import org.junit.*; +import org.junit.rules.TestName; + +import static org.junit.Assert.assertEquals; + +public class AnalyticsTest { + + public static final String KEY = "00112233FF99"; + + private Cloudinary cloudinary; + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary("cloudinary://a:b@test123?load_strategies=false"); + } + + @Test + public void testEncodeVersion() { + Analytics analytics = new Analytics(); + analytics.setSDKSemver("1.24.0"); + analytics.setTechVersion("12.0.0"); + String result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAGAlhAMZAA0"); + + analytics.setSDKSemver("12.0"); + result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAGAMAMZAA0"); + + analytics.setSDKSemver("43.21.26"); + result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAG///AMZAA0"); + + analytics.setSDKSemver("0.0.0"); + result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAGAAAAMZAA0"); + + analytics.setSDKSemver("43.21.27"); + result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=E"); + + } + + @Test + public void testToQueryParam() { + Analytics analytics = new Analytics("F", "2.0.0", "1.8.0", "Z", "1.34.0", "0"); + String result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAFAACMhZBi0"); + + analytics = new Analytics("F", "2.0.0", "1.8.0", "Z", "16.3", "0"); + result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAFAACMhZQD0"); + } + + @Test + public void testUrlWithAnalytics() { + cloudinary.config.analytics = true; + cloudinary.setAnalytics(new Analytics("F", "2.0.0", "1.8.0", "Z", "1.34.0", "0")); + String url = cloudinary.url().generate("test"); + Assert.assertEquals(url, "https://res.cloudinary.com/test123/image/upload/test?_a=DAFAACMhZBi0"); + } + + @Test + public void testUrlWithNoAnalytics() { + cloudinary.config.analytics = false; + String url = cloudinary.url().secure(true).generate("test"); + Assert.assertEquals(url, "https://res.cloudinary.com/test123/image/upload/test"); + } + + @Test + public void testUrlWithNoAnalyticsDefined() { + cloudinary.config.analytics = false; + String url = cloudinary.url().generate("test"); + Assert.assertEquals(url, "https://res.cloudinary.com/test123/image/upload/test"); + } + + @Test + public void testUrlWithNoAnalyticsNull() { + cloudinary.config.analytics = false; + String url = cloudinary.url().generate("test"); + Assert.assertEquals(url, "https://res.cloudinary.com/test123/image/upload/test"); + } + + @Test + public void testUrlWithNoAnalyticsNullAndTrue() { + cloudinary.config.analytics = true; + cloudinary.analytics.setSDKSemver("1.30.0"); + cloudinary.analytics.setTechVersion("12.0.0"); + String url = cloudinary.url().generate("test"); + Assert.assertEquals(url, "https://res.cloudinary.com/test123/image/upload/test?_a=DAGAu5AMZAA0"); + } + + @Test + public void testMiscAnalyticsObject() { + cloudinary.config.analytics = true; + Analytics analytics = new Analytics("Z", "1.24.0", "12.0.0", "Z", "1.34.0", "0"); + String result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAZAlhAMZBi0"); + } + + @Test + public void testErrorAnalytics() { + cloudinary.config.analytics = true; + Analytics analytics = new Analytics("Z", "1.24.0", "0", "Z", "1.34.0", "0"); + String result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=E"); + } + + @Test + public void testUrlNoAnalyticsWithQueryParams() { + final AuthToken authToken = new AuthToken(KEY).duration(300); + authToken.startTime(11111111); // start time is set for test purposes + cloudinary.config.authToken = authToken; + cloudinary.config.cloudName = "test123"; + + cloudinary.config.analytics = true; + cloudinary.setAnalytics(new Analytics("F", "2.0.0", System.getProperty("java.version"), "Z", System.getProperty("os.version"), "0")); + cloudinary.config.privateCdn = true; + String url = cloudinary.url().signed(true).type("authenticated").generate("test"); + assertEquals(url,"https://test123-res.cloudinary.com/image/authenticated/test?__cld_token__=st=11111111~exp=11111411~hmac=735a49389a72ac0b90d1a84ac5d43facd1a9047f153b39e914747ef6ed195e53"); + cloudinary.config.privateCdn = false; + } + + @Test + public void testFeatureFlag() { + Analytics analytics = new Analytics("F", "2.0.0", "1.8.0", "Z", "1.34.0", "0"); + analytics.setFeatureFlag("F"); + String result = analytics.toQueryParam(); + Assert.assertEquals(result, "_a=DAFAACMhZBiF"); + } + + @After + public void tearDown() { + cloudinary.config.analytics = false; + cloudinary.analytics = null; + } + +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/api/signing/ApiResponseSignatureVerifierTest.java b/cloudinary-core/src/test/java/com/cloudinary/api/signing/ApiResponseSignatureVerifierTest.java new file mode 100644 index 00000000..5449f555 --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/api/signing/ApiResponseSignatureVerifierTest.java @@ -0,0 +1,36 @@ +package com.cloudinary.api.signing; + +import com.cloudinary.SignatureAlgorithm; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class ApiResponseSignatureVerifierTest { + @Test + public void testVerifySignature() { + ApiResponseSignatureVerifier verifier = new ApiResponseSignatureVerifier("X7qLTrsES31MzxxkxPPA-pAGGfU"); + + boolean actual = verifier.verifySignature("tests/logo.png", "1", "08d3107a5b2ad82e7d82c0b972218fbf20b5b1e0"); + + assertTrue(actual); + } + + @Test + public void testVerifySignatureFail() { + ApiResponseSignatureVerifier verifier = new ApiResponseSignatureVerifier("X7qLTrsES31MzxxkxPPA-pAGGfU"); + + boolean actual = verifier.verifySignature("tests/logo.png", "1", "doesNotMatchForSure"); + + assertFalse(actual); + } + + @Test + public void testVerifySignatureSHA256() { + ApiResponseSignatureVerifier verifier = new ApiResponseSignatureVerifier("X7qLTrsES31MzxxkxPPA-pAGGfU", SignatureAlgorithm.SHA256); + + boolean actual = verifier.verifySignature("tests/logo.png", "1", "cc69ae4ed73303fbf4a55f2ae5fc7e34ad3a5c387724bfcde447a2957cacdfea"); + + assertTrue(actual); + } +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifierTest.java b/cloudinary-core/src/test/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifierTest.java new file mode 100644 index 00000000..a5d2e096 --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/api/signing/NotificationRequestSignatureVerifierTest.java @@ -0,0 +1,84 @@ +package com.cloudinary.api.signing; + +import com.cloudinary.SignatureAlgorithm; +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class NotificationRequestSignatureVerifierTest { + @Test + public void testVerifySignature() { + NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret"); + + boolean actual = verifier.verifySignature( + "{}", + "0", + "f9aa4471d2a88ff244424cca2444edf7d7ac3596"); + + assertTrue(actual); + } + + @Test + public void testVerifySignatureFailWhenSignatureDoesntMatch() { + NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret"); + + boolean actual = verifier.verifySignature( + "{}", + "0", + "notMatchingForSure"); + + assertFalse(actual); + } + + @Test + public void testVerifySignatureFailWhenTooOld() { + NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret"); + + boolean actual = verifier.verifySignature( + "{}", + "0", + "f9aa4471d2a88ff244424cca2444edf7d7ac3596", + 1000L); + + assertFalse(actual); + } + + @Test + public void testVerifySignaturePassWhenStillValid() { + NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret"); + + boolean actual = verifier.verifySignature( + "{}", + "0", + "f9aa4471d2a88ff244424cca2444edf7d7ac3596", + Long.MAX_VALUE / 1000L); + + assertTrue(actual); + } + + @Test + public void testVerifySignatureFailWhenStillValidButSignatureDoesntMatch() { + NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret"); + + boolean actual = verifier.verifySignature( + "{}", + "0", + "notMatchingForSure", + Long.MAX_VALUE / 1000L); + + assertFalse(actual); + } + + @Test + public void testVerifySignatureSHA256() { + NotificationRequestSignatureVerifier verifier = new NotificationRequestSignatureVerifier("someApiSecret", SignatureAlgorithm.SHA256); + + boolean actual = verifier.verifySignature( + "{}", + "0", + "d5497e1a206ad0ba29ad09a7c0c5f22e939682d15009c15ab3199f62fefbd14b"); + + assertTrue(actual); + } +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/test/ApiTest.java b/cloudinary-core/src/test/java/com/cloudinary/test/ApiTest.java deleted file mode 100644 index fa4b49db..00000000 --- a/cloudinary-core/src/test/java/com/cloudinary/test/ApiTest.java +++ /dev/null @@ -1,658 +0,0 @@ -package com.cloudinary.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assume.assumeNotNull; - -import java.io.IOException; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import com.cloudinary.Api; -import com.cloudinary.Api.ApiResponse; -import com.cloudinary.Cloudinary; -import com.cloudinary.Coordinates; -import com.cloudinary.Transformation; - -@SuppressWarnings({"rawtypes", "unchecked"}) -public class ApiTest { - - private Cloudinary cloudinary; - private Api api; - private static String uniqueTag = String.format("api_test_tag_%d", new java.util.Date().getTime()); - - @BeforeClass - public static void setUpClass() throws IOException { - Cloudinary cloudinary = new Cloudinary(); - if (cloudinary.getStringConfig("api_secret") == null) { - System.err.println("Please setup environment for Upload test to run"); - return; - } - Api api = cloudinary.api(); - try { - api.deleteResources(Arrays.asList("api_test", "api_test1", "api_test2", "api_test3", "api_test5"), Cloudinary.emptyMap()); - } catch (Exception e) { - } - try { - api.deleteTransformation("api_test_transformation", Cloudinary.emptyMap()); - } catch (Exception e) { - } - try { - api.deleteTransformation("api_test_transformation2", Cloudinary.emptyMap()); - } catch (Exception e) { - } - try { - api.deleteTransformation("api_test_transformation3", Cloudinary.emptyMap()); - } catch (Exception e) { - } - try{api.deleteUploadPreset("api_test_upload_preset", Cloudinary.emptyMap());}catch (Exception e) {} - try{api.deleteUploadPreset("api_test_upload_preset2", Cloudinary.emptyMap());}catch (Exception e) {} - try{api.deleteUploadPreset("api_test_upload_preset3", Cloudinary.emptyMap());}catch (Exception e) {} - try{api.deleteUploadPreset("api_test_upload_preset4", Cloudinary.emptyMap());}catch (Exception e) {} - Map options = Cloudinary.asMap( - "public_id", "api_test", - "tags", new String[]{"api_test_tag", uniqueTag}, - "context", "key=value", - "eager", Collections.singletonList(new Transformation().width(100).crop("scale"))); - cloudinary.uploader().upload("src/test/resources/logo.png", options); - options.put("public_id", "api_test1"); - cloudinary.uploader().upload("src/test/resources/logo.png", options); - } - - @Before - public void setUp() { - this.cloudinary = new Cloudinary(); - assumeNotNull(cloudinary.getStringConfig("api_secret")); - this.api = cloudinary.api(); - } - - public Map findByAttr(List elements, String attr, Object value) { - for (Map element : elements) { - if (value.equals(element.get(attr))) { - return element; - } - } - return null; - } - - @Test - public void test01ResourceTypes() throws Exception { - // should allow listing resource_types - Map result = api.resourceTypes(Cloudinary.emptyMap()); - assertContains("image", (Collection) result.get("resource_types")); - } - - @Test - public void test02Resources() throws Exception { - // should allow listing resources - Map result = api.resources(Cloudinary.emptyMap()); - Map resource = findByAttr((List) result.get("resources"), "public_id", "api_test"); - assertNotNull(resource); - assertEquals(resource.get("type"), "upload"); - } - - @Test - public void test03ResourcesCursor() throws Exception { - // should allow listing resources with cursor - Map options = new HashMap(); - options.put("max_results", 1); - Map result = api.resources(options); - List resources = (List) result.get("resources"); - assertNotNull(resources); - assertEquals(1, resources.size()); - assertNotNull(result.get("next_cursor")); - - options.put("next_cursor", result.get("next_cursor")); - Map result2 = api.resources(options); - List resources2 = (List) result2.get("resources"); - assertNotNull(resources2); - assertEquals(resources2.size(), 1); - assertNotSame(resources2.get(0).get("public_id"), resources.get(0).get("public_id")); - } - - @Test - public void test04ResourcesByType() throws Exception { - // should allow listing resources by type - Map result = api.resources(Cloudinary.asMap("type", "upload")); - Map resource = findByAttr((List) result.get("resources"), "public_id", "api_test"); - assertNotNull(resource); - } - - @Test - public void test05ResourcesByPrefix() throws Exception { - // should allow listing resources by prefix - Map result = api.resources(Cloudinary.asMap("type", "upload", "prefix", "api_test", "tags", true, "context", true)); - List resources = (List) result.get("resources"); - assertNotNull(findByAttr(resources, "public_id", "api_test")); - assertNotNull(findByAttr(resources, "public_id", "api_test1")); - assertNotNull(findByAttr((List) result.get("resources"), "context", Cloudinary.asMap("custom", Cloudinary.asMap("key", "value")))); - boolean found = false; - for (Map r: resources) { - org.json.simple.JSONArray tags = (org.json.simple.JSONArray) r.get("tags"); - found = found || tags.contains("api_test_tag"); - } - assertTrue(found); - } - - @Test - public void testResourcesListingDirection() throws Exception { - // should allow listing resources in both directions - Map result = api.resourcesByTag(uniqueTag, Cloudinary.asMap("type", "upload", "direction", "asc")); - List resources = (List) result.get("resources"); - result = api.resourcesByTag(uniqueTag, Cloudinary.asMap("type", "upload", "direction", -1)); - List resourcesDesc = (List) result.get("resources"); - Collections.reverse(resources); - assertEquals(resources, resourcesDesc); - } - - @Test - public void testResourcesListingStartAt() throws Exception { - // should allow listing resources by start date - make sure your clock is set correctly!!! - Thread.sleep(2000L); - java.util.Date startAt = new java.util.Date(); - Thread.sleep(2000L); - Map response = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.emptyMap()); - List resources = (List) api.resources(Cloudinary.asMap("type", "upload", "start_at", startAt, "direction", "asc")).get("resources"); - assertEquals(response.get("public_id"), resources.get(0).get("public_id")); - } - - @Test - public void testResourcesByPublicIds() throws Exception { - // should allow listing resources by public ids - Map result = api.resourcesByIds(Arrays.asList("api_test", "api_test1", "bogus"), Cloudinary.asMap("type", "upload", "tags", true, "context", true)); - List resources = (List) result.get("resources"); - assertEquals(2, resources.size()); - assertNotNull(findByAttr(resources, "public_id", "api_test")); - assertNotNull(findByAttr(resources, "public_id", "api_test1")); - assertNotNull(findByAttr((List) result.get("resources"), "context", Cloudinary.asMap("custom", Cloudinary.asMap("key", "value")))); - boolean found = false; - for (Map r: resources) { - org.json.simple.JSONArray tags = (org.json.simple.JSONArray) r.get("tags"); - found = found || tags.contains("api_test_tag"); - } - assertTrue(found); - } - - @Test - public void test06ResourcesTag() throws Exception { - // should allow listing resources by tag - Map result = api.resourcesByTag("api_test_tag", Cloudinary.asMap("tags", true, "context", true)); - Map resource = findByAttr((List) result.get("resources"), "public_id", "api_test"); - assertNotNull(resource); - resource = findByAttr((List) result.get("resources"), "context", Cloudinary.asMap("custom", Cloudinary.asMap("key", "value"))); - assertNotNull(resource); - List resources = (List) result.get("resources"); - boolean found = false; - for (Map r: resources) { - org.json.simple.JSONArray tags = (org.json.simple.JSONArray) r.get("tags"); - found = found || tags.contains("api_test_tag"); - } - assertTrue(found); - } - - @Test - public void test07ResourceMetadata() throws Exception { - // should allow get resource metadata - Map resource = api.resource("api_test", Cloudinary.emptyMap()); - assertNotNull(resource); - assertEquals(resource.get("public_id"), "api_test"); - assertEquals(resource.get("bytes"), 3381L); - assertEquals(((List) resource.get("derived")).size(), 1); - } - - @Test - public void test08DeleteDerived() throws Exception { - // should allow deleting derived resource - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap( - "public_id", "api_test3", - "eager", Collections.singletonList(new Transformation().width(101).crop("scale")) - )); - Map resource = api.resource("api_test3", Cloudinary.emptyMap()); - assertNotNull(resource); - List derived = (List) resource.get("derived"); - assertEquals(derived.size(), 1); - String derived_resource_id = (String) derived.get(0).get("id"); - api.deleteDerivedResources(Arrays.asList(derived_resource_id), Cloudinary.emptyMap()); - resource = api.resource("api_test3", Cloudinary.emptyMap()); - assertNotNull(resource); - derived = (List) resource.get("derived"); - assertEquals(derived.size(), 0); - } - - @Test(expected = Api.NotFound.class) - public void test09DeleteResources() throws Exception { - // should allow deleting resources - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("public_id", "api_test3")); - Map resource = api.resource("api_test3", Cloudinary.emptyMap()); - assertNotNull(resource); - api.deleteResources(Arrays.asList("apit_test", "api_test2", "api_test3"), Cloudinary.emptyMap()); - api.resource("api_test3", Cloudinary.emptyMap()); - } - - @Test(expected = Api.NotFound.class) - public void test09aDeleteResourcesByPrefix() throws Exception { - // should allow deleting resources - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("public_id", "api_test_by_prefix")); - Map resource = api.resource("api_test_by_prefix", Cloudinary.emptyMap()); - assertNotNull(resource); - api.deleteResourcesByPrefix("api_test_by", Cloudinary.emptyMap()); - api.resource("api_test_by_prefix", Cloudinary.emptyMap()); - } - - @Test(expected = Api.NotFound.class) - public void test09aDeleteResourcesByTags() throws Exception { - // should allow deleting resources - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("public_id", "api_test4", "tags", Arrays.asList("api_test_tag_for_delete"))); - Map resource = api.resource("api_test4", Cloudinary.emptyMap()); - assertNotNull(resource); - api.deleteResourcesByTag("api_test_tag_for_delete", Cloudinary.emptyMap()); - api.resource("api_test4", Cloudinary.emptyMap()); - } - - @Test - public void test10Tags() throws Exception { - // should allow listing tags - Map result = api.tags(Cloudinary.emptyMap()); - List tags = (List) result.get("tags"); - assertContains("api_test_tag", tags); - } - - @Test - public void test11TagsPrefix() throws Exception { - // should allow listing tag by prefix - Map result = api.tags(Cloudinary.asMap("prefix", "api_test")); - List tags = (List) result.get("tags"); - assertContains("api_test_tag", tags); - result = api.tags(Cloudinary.asMap("prefix", "api_test_no_such_tag")); - tags = (List) result.get("tags"); - assertEquals(0, tags.size()); - } - - @Test - public void test12Transformations() throws Exception { - // should allow listing transformations - Map result = api.transformations(Cloudinary.emptyMap()); - Map transformation = findByAttr((List) result.get("transformations"), "name", "c_scale,w_100"); - - assertNotNull(transformation); - assertTrue((Boolean) transformation.get("used")); - } - - @Test - public void test13TransformationMetadata() throws Exception { - // should allow getting transformation metadata - Map transformation = api.transformation("c_scale,w_100", Cloudinary.emptyMap()); - assertNotNull(transformation); - assertEquals(new Transformation((List) transformation.get("info")).generate(), new Transformation().crop("scale").width(100) - .generate()); - } - - @Test - public void test14TransformationUpdate() throws Exception { - // should allow updating transformation allowed_for_strict - api.updateTransformation("c_scale,w_100", Cloudinary.asMap("allowed_for_strict", true), Cloudinary.emptyMap()); - Map transformation = api.transformation("c_scale,w_100", Cloudinary.emptyMap()); - assertNotNull(transformation); - assertEquals(transformation.get("allowed_for_strict"), true); - api.updateTransformation("c_scale,w_100", Cloudinary.asMap("allowed_for_strict", false), Cloudinary.emptyMap()); - transformation = api.transformation("c_scale,w_100", Cloudinary.emptyMap()); - assertNotNull(transformation); - assertEquals(transformation.get("allowed_for_strict"), false); - } - - @Test - public void test15TransformationCreate() throws Exception { - // should allow creating named transformation - api.createTransformation("api_test_transformation", new Transformation().crop("scale").width(102).generate(), Cloudinary.emptyMap()); - Map transformation = api.transformation("api_test_transformation", Cloudinary.emptyMap()); - assertNotNull(transformation); - assertEquals(transformation.get("allowed_for_strict"), true); - assertEquals(new Transformation((List) transformation.get("info")).generate(), new Transformation().crop("scale").width(102) - .generate()); - assertEquals(transformation.get("used"), false); - } - - @Test - public void test15aTransformationUnsafeUpdate() throws Exception { - // should allow unsafe update of named transformation - api.createTransformation("api_test_transformation3", new Transformation().crop("scale").width(102).generate(), Cloudinary.emptyMap()); - api.updateTransformation("api_test_transformation3", Cloudinary.asMap("unsafe_update", new Transformation().crop("scale").width(103).generate()), Cloudinary.emptyMap()); - Map transformation = api.transformation("api_test_transformation3", Cloudinary.emptyMap()); - assertNotNull(transformation); - assertEquals(new Transformation((List) transformation.get("info")).generate(), new Transformation().crop("scale").width(103) - .generate()); - assertEquals(transformation.get("used"), false); - } - - @Test - public void test16aTransformationDelete() throws Exception { - // should allow deleting named transformation - api.createTransformation("api_test_transformation2", new Transformation().crop("scale").width(103).generate(), Cloudinary.emptyMap()); - api.transformation("api_test_transformation2", Cloudinary.emptyMap()); - api.deleteTransformation("api_test_transformation2", Cloudinary.emptyMap()); - } - - @Test(expected = Api.NotFound.class) - public void test16bTransformationDelete() throws Exception { - api.transformation("api_test_transformation2", Cloudinary.emptyMap()); - } - - @Test - public void test17aTransformationDeleteImplicit() throws Exception { - // should allow deleting implicit transformation - api.transformation("c_scale,w_100", Cloudinary.emptyMap()); - api.deleteTransformation("c_scale,w_100", Cloudinary.emptyMap()); - } - - /** - * @throws Exception - * @expectedException \Cloudinary\Api\NotFound - */ - @Test(expected = Api.NotFound.class) - public void test17bTransformationDeleteImplicit() throws Exception { - api.transformation("c_scale,w_100", Cloudinary.emptyMap()); - } - - @Test - public void test18Usage() throws Exception { - // should support usage API call - Map result = api.usage(Cloudinary.emptyMap()); - assertNotNull(result.get("last_updated")); - } - - @Test - public void test19Ping() throws Exception { - // should support ping API call - Map result = api.ping(Cloudinary.emptyMap()); - assertEquals(result.get("status"), "ok"); - } - - // This test must be last because it deletes (potentially) all dependent transformations which some tests rely on. - // Add @Test if you really want to test it - This test deletes derived resources! - public void testDeleteAllResources() throws Exception { - // should allow deleting all resources - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("public_id", "api_test5", "eager", Collections.singletonList(new Transformation().crop("scale").width(2.0)))); - Map result = api.resource("api_test5", Cloudinary.emptyMap()); - assertEquals(1, ((org.json.simple.JSONArray) result.get("derived")).size()); - api.deleteAllResources(Cloudinary.asMap("keep_original", true)); - result = api.resource("api_test5", Cloudinary.emptyMap()); - //assertEquals(0, ((org.json.simple.JSONArray) result.get("derived")).size()); - } - - - @Test - public void testManualModeration() throws Exception { - // should support setting manual moderation status - Map uploadResult = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("moderation","manual")); - Map apiResult = api.update((String) uploadResult.get("public_id"), Cloudinary.asMap("moderation_status", "approved")); - assertEquals("approved", ((Map) ((List) apiResult.get("moderation")).get(0)).get("status")); - } - - @Test - public void testOcrUpdate() { - // should support requesting ocr info - try { - Map uploadResult = cloudinary.uploader().upload( - "src/test/resources/logo.png", Cloudinary.emptyMap()); - api.update((String) uploadResult.get("public_id"), - Cloudinary.asMap("ocr", "illegal")); - } catch (Exception e) { - assertTrue(e instanceof com.cloudinary.Api.BadRequest); - assertTrue(e.getMessage().matches("^Illegal value(.*)")); - } - } - - @Test - public void testRawConvertUpdate() { - // should support requesting raw conversion - try { - Map uploadResult = cloudinary.uploader().upload( - "src/test/resources/logo.png", Cloudinary.emptyMap()); - api.update((String) uploadResult.get("public_id"), - Cloudinary.asMap("raw_convert", "illegal")); - } catch (Exception e) { - assertTrue(e instanceof com.cloudinary.Api.BadRequest); - assertTrue(e.getMessage().matches("^Illegal value(.*)")); - } - } - - @Test - public void testCategorizationUpdate() { - // should support requesting categorization - try { - Map uploadResult = cloudinary.uploader().upload( - "src/test/resources/logo.png", Cloudinary.emptyMap()); - api.update((String) uploadResult.get("public_id"), - Cloudinary.asMap("categorization", "illegal")); - } catch (Exception e) { - assertTrue(e instanceof com.cloudinary.Api.BadRequest); - assertTrue(e.getMessage().matches("^Illegal value(.*)")); - } - } - - @Test - public void testDetectionUpdate() { - // should support requesting detection - try { - Map uploadResult = cloudinary.uploader().upload( - "src/test/resources/logo.png", Cloudinary.emptyMap()); - api.update((String) uploadResult.get("public_id"), - Cloudinary.asMap("detection", "illegal")); - } catch (Exception e) { - assertTrue(e instanceof com.cloudinary.Api.BadRequest); - assertTrue(e.getMessage().matches("^Illegal value(.*)")); - } - } - - @Test - public void testSimilaritySearchUpdate() { - // should support requesting similarity search - try { - Map uploadResult = cloudinary.uploader().upload( - "src/test/resources/logo.png", Cloudinary.emptyMap()); - api.update((String) uploadResult.get("public_id"), - Cloudinary.asMap("similarity_search", "illegal")); - } catch (Exception e) { - assertTrue(e instanceof com.cloudinary.Api.BadRequest); - assertTrue(e.getMessage().matches("^Illegal value(.*)")); - } - } - - @Test - public void testUpdateCustomCoordinates() throws IOException, Exception { - //should update custom coordinates - Coordinates coordinates = new Coordinates("121,31,110,151"); - Map uploadResult = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.emptyMap()); - cloudinary.api().update(uploadResult.get("public_id").toString(), Cloudinary.asMap("custom_coordinates", coordinates)); - Map result = cloudinary.api().resource(uploadResult.get("public_id").toString(), Cloudinary.asMap("coordinates", true)); - long[] expected = new long[]{121L,31L,110L,151L}; - Object[] actual = ((org.json.simple.JSONArray)((org.json.simple.JSONArray)((Map)result.get("coordinates")).get("custom")).get(0)).toArray(); - for (int i = 0; i < expected.length; i++){ - assertEquals(expected[i], actual[i]); - } - } - - @Test - public void testApiLimits() throws Exception { - // should support reporting the current API limits found in the response header - ApiResponse result1 = api.transformations(Cloudinary.emptyMap()); - ApiResponse result2 = api.transformations(Cloudinary.emptyMap()); - assertNotNull(result1.apiRateLimit()); - assertNotNull(result2.apiRateLimit()); - assertEquals(result1.apiRateLimit().getRemaining() - 1, result2.apiRateLimit().getRemaining()); - assertTrue(result2.apiRateLimit().getLimit() > result2.apiRateLimit().getRemaining()); - assertEquals(result1.apiRateLimit().getLimit(), result2.apiRateLimit().getLimit()); - assertEquals(result1.apiRateLimit().getReset(), result2.apiRateLimit().getReset()); - assertTrue(result2.apiRateLimit().getReset().after(new java.util.Date())); - } - - @Test - public void testListUploadPresets() throws Exception { - // should allow creating and listing upload_presets - api.createUploadPreset(Cloudinary.asMap("name", - "api_test_upload_preset", "folder", "folder")); - api.createUploadPreset(Cloudinary.asMap("name", - "api_test_upload_preset2", "folder", "folder2")); - api.createUploadPreset(Cloudinary.asMap("name", - "api_test_upload_preset3", "folder", "folder3")); - org.json.simple.JSONArray presets = (org.json.simple.JSONArray) api - .uploadPresets(Cloudinary.emptyMap()).get("presets"); - assertEquals(((Map) presets.get(0)).get("name"), - "api_test_upload_preset3"); - assertEquals(((Map) presets.get(1)).get("name"), - "api_test_upload_preset2"); - assertEquals(((Map) presets.get(2)).get("name"), - "api_test_upload_preset"); - api.deleteUploadPreset("api_test_upload_preset", Cloudinary.emptyMap()); - api.deleteUploadPreset("api_test_upload_preset2", Cloudinary.emptyMap()); - api.deleteUploadPreset("api_test_upload_preset3", Cloudinary.emptyMap()); - } - - @Test - public void testGetUploadPreset() throws Exception { - // should allow getting a single upload_preset - String[] tags = { "a", "b", "c" }; - Map context = Cloudinary.asMap("a", "b", "c", "d"); - Transformation transformation = new Transformation(); - transformation.width(100).crop("scale"); - Map result = api.createUploadPreset(Cloudinary.asMap("unsigned", true, - "folder", "folder", "transformation", transformation, "tags", - tags, "context", context)); - String name = result.get("name").toString(); - Map preset = api.uploadPreset(name, Cloudinary.emptyMap()); - assertEquals(preset.get("name"), name); - assertEquals(Boolean.TRUE, preset.get("unsigned")); - Map settings = (Map) preset.get("settings"); - assertEquals(settings.get("folder"), "folder"); - Map outTransformation = (Map) ((org.json.simple.JSONArray) settings - .get("transformation")).get(0); - assertEquals(outTransformation.get("width"), 100L); - assertEquals(outTransformation.get("crop"), "scale"); - Object[] outTags = ((org.json.simple.JSONArray) settings.get("tags")) - .toArray(); - assertArrayEquals(tags, outTags); - Map outContext = (Map) settings.get("context"); - assertEquals(context, outContext); - } - - @Test - public void testDeleteUploadPreset() throws Exception { - // should allow deleting upload_presets", :upload_preset => true do - api.createUploadPreset(Cloudinary.asMap("name", - "api_test_upload_preset4", "folder", "folder")); - api.uploadPreset("api_test_upload_preset4", Cloudinary.emptyMap()); - api.deleteUploadPreset("api_test_upload_preset4", Cloudinary.emptyMap()); - boolean error = false; - try { - api.uploadPreset("api_test_upload_preset4", Cloudinary.emptyMap()); - } catch (Exception e) { - error = true; - } - assertTrue(error); - } - - @Test - public void testUpdateUploadPreset() throws Exception { - // should allow updating upload_presets - String name = api - .createUploadPreset(Cloudinary.asMap("folder", "folder")) - .get("name").toString(); - Map preset = api.uploadPreset(name, Cloudinary.emptyMap()); - Map settings = (Map) preset.get("settings"); - settings.putAll(Cloudinary.asMap("colors", true, "unsigned", true, - "disallow_public_id", true)); - api.updateUploadPreset(name, settings); - settings.remove("unsigned"); - preset = api.uploadPreset(name, Cloudinary.emptyMap()); - assertEquals(name, preset.get("name")); - assertEquals(Boolean.TRUE, preset.get("unsigned")); - assertEquals(settings, preset.get("settings")); - api.deleteUploadPreset(name, Cloudinary.emptyMap()); - } - - @Test - public void testListByModerationUpdate() throws Exception { - // "should support listing by moderation kind and value - Map result1 = cloudinary.uploader().upload( - "src/test/resources/logo.png", - Cloudinary.asMap("moderation", "manual")); - Map result2 = cloudinary.uploader().upload( - "src/test/resources/logo.png", - Cloudinary.asMap("moderation", "manual")); - Map result3 = cloudinary.uploader().upload( - "src/test/resources/logo.png", - Cloudinary.asMap("moderation", "manual")); - api.update((String) result1.get("public_id"), - Cloudinary.asMap("moderation_status", "approved")); - api.update((String) result2.get("public_id"), - Cloudinary.asMap("moderation_status", "rejected")); - Map approved = api.resourcesByModeration("manual", "approved", - Cloudinary.asMap("max_results", 1000)); - Map rejected = api.resourcesByModeration("manual", "rejected", - Cloudinary.asMap("max_results", 1000)); - Map pending = api.resourcesByModeration("manual", "pending", - Cloudinary.asMap("max_results", 1000)); - assertNotNull(findByAttr((List) approved.get("resources"), - "public_id", (String) result1.get("public_id"))); - assertNull(findByAttr((List) approved.get("resources"), - "public_id", (String) result2.get("public_id"))); - assertNull(findByAttr((List) approved.get("resources"), - "public_id", (String) result2.get("public_id"))); - assertNotNull(findByAttr((List) rejected.get("resources"), - "public_id", (String) result2.get("public_id"))); - assertNull(findByAttr((List) rejected.get("resources"), - "public_id", (String) result1.get("public_id"))); - assertNull(findByAttr((List) rejected.get("resources"), - "public_id", (String) result3.get("public_id"))); - assertNotNull(findByAttr((List) pending.get("resources"), - "public_id", (String) result3.get("public_id"))); - assertNull(findByAttr((List) pending.get("resources"), - "public_id", (String) result1.get("public_id"))); - assertNull(findByAttr((List) pending.get("resources"), - "public_id", (String) result2.get("public_id"))); - } - - // For this test to work, "Auto-create folders" should be enabled in the - // Upload Settings. - // Uncomment @Test if you really want to test it. - // @Test - public void testFolderApi() throws Exception { - // should allow deleting all resources - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("public_id", "test_folder1/item")); - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("public_id", "test_folder2/item")); - cloudinary.uploader().upload("src/test/resources/logo.png", - Cloudinary.asMap("public_id", "test_folder1/test_subfolder1/item")); - cloudinary.uploader().upload("src/test/resources/logo.png", - Cloudinary.asMap("public_id", "test_folder1/test_subfolder2/item")); - Map result = api.rootFolders(null); - assertEquals("test_folder1", ((Map) ((org.json.simple.JSONArray) result.get("folders")).get(0)).get("name")); - assertEquals("test_folder2", ((Map) ((org.json.simple.JSONArray) result.get("folders")).get(1)).get("name")); - result = api.subFolders("test_folder1", null); - assertEquals("test_folder1/test_subfolder1", - ((Map) ((org.json.simple.JSONArray) result.get("folders")).get(0)).get("path")); - assertEquals("test_folder1/test_subfolder2", - ((Map) ((org.json.simple.JSONArray) result.get("folders")).get(1)).get("path")); - try { - api.subFolders("test_folder", null); - } catch (Exception e) { - assertTrue(e instanceof com.cloudinary.Api.NotFound); - } - api.deleteResourcesByPrefix("test_folder", Cloudinary.emptyMap()); - } - - private void assertContains(Object object, Collection list) { - assertTrue(list.contains(object)); - } -} diff --git a/cloudinary-core/src/test/java/com/cloudinary/test/CloudinaryTest.java b/cloudinary-core/src/test/java/com/cloudinary/test/CloudinaryTest.java index 29439b03..c40a3ea2 100644 --- a/cloudinary-core/src/test/java/com/cloudinary/test/CloudinaryTest.java +++ b/cloudinary-core/src/test/java/com/cloudinary/test/CloudinaryTest.java @@ -1,485 +1,1532 @@ package com.cloudinary.test; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; +import com.cloudinary.*; +import com.cloudinary.transformation.*; +import com.cloudinary.utils.ObjectUtils; +import junitparams.JUnitParamsRunner; +import junitparams.Parameters; +import junitparams.naming.TestCaseName; +import org.cloudinary.json.JSONObject; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; +import org.junit.runner.RunWith; +import java.io.IOException; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; import java.net.URI; +import java.net.URISyntaxException; import java.net.URLDecoder; -import java.util.HashMap; -import java.util.Map; +import java.net.URLEncoder; +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; -import org.junit.Before; -import org.junit.Test; - -import com.cloudinary.Cloudinary; -import com.cloudinary.Transformation; +import static com.cloudinary.CustomFunction.remote; +import static com.cloudinary.CustomFunction.wasm; +import static com.cloudinary.utils.ObjectUtils.asMap; +import static com.cloudinary.utils.ObjectUtils.emptyMap; +import static org.junit.Assert.*; +@RunWith(JUnitParamsRunner.class) public class CloudinaryTest { + private static final String DEFAULT_ROOT_PATH = "https://res.cloudinary.com/test123/"; + private static final String DEFAULT_UPLOAD_PATH = DEFAULT_ROOT_PATH + "image/upload/"; + private static final String VIDEO_UPLOAD_PATH = DEFAULT_ROOT_PATH + "video/upload/"; + private Cloudinary cloudinary; + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary("cloudinary://a:b@test123?load_strategies=false&analytics=false"); + } + + @Test + public void testUrlSuffixWithDotOrSlash() { + Boolean[] errors = new Boolean[4]; + try { + cloudinary.url().suffix("dsfdfd.adsfad").generate("publicId"); + } catch (IllegalArgumentException e) { + errors[0] = true; + } + + try { + cloudinary.url().suffix("dsfdfd/adsfad").generate("publicId"); + } catch (IllegalArgumentException e) { + errors[1] = true; + } + + try { + cloudinary.url().suffix("dsfd.fd/adsfad").generate("publicId"); + } catch (IllegalArgumentException e) { + errors[2] = true; + } + + try { + cloudinary.url().suffix("dsfdfdaddsfad").generate("publicId"); + } catch (IllegalArgumentException e) { + errors[3] = true; + } + + assertTrue(errors[0]); + assertTrue(errors[1]); + assertTrue(errors[2]); + assertNull(errors[3]); + } + + @Test + public void testCloudName() { + // should use cloud_name from config + String result = cloudinary.url().generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "test", result); + } + + @Test + public void testCloudNameOptions() { + // should allow overriding cloud_name in options + String result = cloudinary.url().cloudName("test321").generate("test"); + assertEquals("https://res.cloudinary.com/test321/image/upload/test", result); + } + + @Test + public void testSecureDistribution() { + // should use default secure distribution if secure=TRUE + String result = cloudinary.url().generate("test"); + assertEquals("https://res.cloudinary.com/test123/image/upload/test", result); + } + + @Test + public void testTextLayerStyleIdentifierVariables() { + String url = cloudinary.url().transformation( + new Transformation() + .variable("$style", "!Arial_12!") + .chain() + .overlay( + new TextLayer().text("hello-world").textStyle("$style") + )).generate("sample"); + + assertEquals("https://res.cloudinary.com/test123/image/upload/$style_!Arial_12!/l_text:$style:hello-world/sample", url); + + url = cloudinary.url().transformation( + new Transformation() + .variable("$style", "!Arial_12!") + .chain() + .overlay( + new TextLayer().text("hello-world").textStyle(new Expression("$style")) + )).generate("sample"); + + assertEquals("https://res.cloudinary.com/test123/image/upload/$style_!Arial_12!/l_text:$style:hello-world/sample", url); + } + + + @Test + public void testSecureDistributionOverwrite() { + // should allow overwriting secure distribution if secure=TRUE + String result = cloudinary.url().secureDistribution("something.else.com").generate("test"); + assertEquals("https://something.else.com/test123/image/upload/test", result); + } + + @Test + public void testSecureDistibution() { + // should take secure distribution from config if secure=TRUE + cloudinary.config.secureDistribution = "config.secure.distribution.com"; + String result = cloudinary.url().secure(true).generate("test"); + assertEquals("https://config.secure.distribution.com/test123/image/upload/test", result); + } + + @Test + public void testSecureAkamai() { + // should default to akamai if secure is given with private_cdn and no + // secure_distribution + cloudinary.config.secure = true; + cloudinary.config.privateCdn = true; + String result = cloudinary.url().generate("test"); + assertEquals("https://test123-res.cloudinary.com/image/upload/test", result); + } + + @Test + public void testSecureNonAkamai() { + // should not add cloud_name if private_cdn and secure non akamai + // secure_distribution + cloudinary.config.secure = true; + cloudinary.config.privateCdn = true; + cloudinary.config.secureDistribution = "something.cloudfront.net"; + String result = cloudinary.url().generate("test"); + assertEquals("https://something.cloudfront.net/image/upload/test", result); + } + + @Test + public void testHttpPrivateCdn() { + // should not add cloud_name if private_cdn and not secure + cloudinary.config.privateCdn = true; + String result = cloudinary.url().generate("test"); + assertEquals("https://test123-res.cloudinary.com/image/upload/test", result); + } + + @Test + public void testFormat() { + // should use format from options + String result = cloudinary.url().format("jpg").generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "test.jpg", result); + } + + @Test + public void testType() { + // should use type from options + String result = cloudinary.url().type("facebook").generate("test"); + assertEquals("https://res.cloudinary.com/test123/image/facebook/test", result); + } + + @Test + public void testResourceType() { + // should use resource_type from options + String result = cloudinary.url().resourcType("raw").generate("test"); + assertEquals("https://res.cloudinary.com/test123/raw/upload/test", result); + } + + @Test + public void testIgnoreHttp() { + // should ignore http links only if type is not given or is asset + String result = cloudinary.url().generate("http://test"); + assertEquals("http://test", result); + result = cloudinary.url().type("asset").generate("http://test"); + assertEquals("http://test", result); + result = cloudinary.url().type("fetch").generate("http://test"); + assertEquals("https://res.cloudinary.com/test123/image/fetch/http://test", result); + } + + @Test + public void testFetch() { + // should escape fetch urls + String result = cloudinary.url().type("fetch").generate("http://blah.com/hello?a=b"); + assertEquals("https://res.cloudinary.com/test123/image/fetch/http://blah.com/hello%3Fa%3Db", result); + } + + @Test + public void testCname() { + // should support external cname + String result = cloudinary.url().cname("hello.com").secure(false).generate("test"); + assertEquals("http://hello.com/test123/image/upload/test", result); + } + + @Test + public void testCnameSubdomain() { + // should support external cname with cdn_subdomain on + String result = cloudinary.url().cname("hello.com").cdnSubdomain(true).secure(false).generate("test"); + assertEquals("http://a2.hello.com/test123/image/upload/test", result); + } + + @Test(expected = IllegalArgumentException.class) + public void testDisallowUrlSuffixInNonUploadTypes() { + cloudinary.url().suffix("hello").privateCdn(true).type("facebook").generate("test"); + + } + + @Test(expected = IllegalArgumentException.class) + public void testDisallowUrlSuffixWithSlash() { + cloudinary.url().suffix("hello/world").privateCdn(true).generate("test"); + } + + @Test(expected = IllegalArgumentException.class) + public void testDisallowUrlSuffixWithDot() { + cloudinary.url().suffix("hello.world").privateCdn(true).generate("test"); + } + + @Test + public void testSupportUrlSuffixForPrivateCdn() { + String actual = cloudinary.url().suffix("hello").privateCdn(true).generate("test"); + assertEquals("https://test123-res.cloudinary.com/images/test/hello", actual); + + actual = cloudinary.url().suffix("hello").privateCdn(true).transformation(new Transformation().angle(0)).generate("test"); + assertEquals("https://test123-res.cloudinary.com/images/a_0/test/hello", actual); + + } + + @Test + public void testPutFormatAfterUrlSuffix() { + String actual = cloudinary.url().suffix("hello").privateCdn(true).format("jpg").generate("test"); + assertEquals("https://test123-res.cloudinary.com/images/test/hello.jpg", actual); + } + + @Test + public void testNotSignTheUrlSuffix() { + + Pattern pattern = Pattern.compile("s--[0-9A-Za-z_-]{8}--"); + String url = cloudinary.url().format("jpg").signed(true).generate("test"); + Matcher matcher = pattern.matcher(url); + matcher.find(); + String expectedSignature = url.substring(matcher.start(), matcher.end()); + + String actual = cloudinary.url().format("jpg").privateCdn(true).signed(true).suffix("hello").generate("test"); + assertEquals("https://test123-res.cloudinary.com/images/" + expectedSignature + "/test/hello.jpg", actual); + + url = cloudinary.url().format("jpg").signed(true).transformation(new Transformation().angle(0)).generate("test"); + matcher = pattern.matcher(url); + matcher.find(); + expectedSignature = url.substring(matcher.start(), matcher.end()); + + actual = cloudinary.url().format("jpg").privateCdn(true).signed(true).suffix("hello").transformation(new Transformation().angle(0)).generate("test"); + + assertEquals("https://test123-res.cloudinary.com/images/" + expectedSignature + "/a_0/test/hello.jpg", actual); + } + + @Test + public void testSignatureLength(){ + String url = cloudinary.url().signed(true).generate("sample.jpg"); + assertEquals("https://res.cloudinary.com/test123/image/upload/s--v2fTPYTu--/sample.jpg", url); + + url = cloudinary.url().signed(true).longUrlSignature(true).generate("sample.jpg"); + assertEquals("https://res.cloudinary.com/test123/image/upload/s--2hbrSMPOjj5BJ4xV7SgFbRDevFaQNUFf--/sample.jpg", url); + } + + @Test + public void testSupportUrlSuffixForRawUploads() { + String actual = cloudinary.url().suffix("hello").privateCdn(true).resourceType("raw").generate("test"); + assertEquals("https://test123-res.cloudinary.com/files/test/hello", actual); + } + + @Test + public void testSupportUrlSuffixForVideoUploads() { + String actual = cloudinary.url().suffix("hello").privateCdn(true).resourceType("video").generate("test"); + assertEquals("https://test123-res.cloudinary.com/videos/test/hello", actual); + } + + @Test + public void testSupportUrlSuffixForAuthenticatedImages() { + String actual = cloudinary.url().suffix("hello").privateCdn(true).resourceType("image").type("authenticated").generate("test"); + assertEquals("https://test123-res.cloudinary.com/authenticated_images/test/hello", actual); + } + + @Test + public void testSupportUrlSuffixForPrivateImages() { + String actual = cloudinary.url().suffix("hello").privateCdn(true).resourceType("image").type("private").generate("test"); + assertEquals("https://test123-res.cloudinary.com/private_images/test/hello", actual); + } + + @Test + public void testSupportUseRootPathForPrivateCdn() { + String actual = cloudinary.url().privateCdn(true).useRootPath(true).generate("test"); + assertEquals("https://test123-res.cloudinary.com/test", actual); + + actual = cloudinary.url().privateCdn(true).transformation(new Transformation().angle(0)).useRootPath(true).generate("test"); + assertEquals("https://test123-res.cloudinary.com/a_0/test", actual); + } + + @Test + public void testSupportUseRootPathTogetherWithUrlSuffixForPrivateCdn() { + + String actual = cloudinary.url().privateCdn(true).suffix("hello").useRootPath(true).generate("test"); + assertEquals("https://test123-res.cloudinary.com/test/hello", actual); + + } + + @Test(expected = IllegalArgumentException.class) + public void testDisllowUseRootPathIfNotImageUploadForFacebook() { + cloudinary.url().useRootPath(true).privateCdn(true).type("facebook").generate("test"); + } + + @Test(expected = IllegalArgumentException.class) + public void testDisllowUseRootPathIfNotImageUploadForRaw() { + cloudinary.url().useRootPath(true).privateCdn(true).resourceType("raw").generate("test"); + } + + @Test + public void testCrop() { + Transformation transformation = new Transformation().width(100).height(101); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "h_101,w_100/test", result); + assertEquals("101", transformation.getHtmlHeight()); + assertEquals("100", transformation.getHtmlWidth()); + transformation = new Transformation().width(100).height(101).crop("crop"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "c_crop,h_101,w_100/test", result); + } + + @Test + public void testVariousOptions() { + // should use x, y, radius, prefix, gravity and quality from options + Transformation transformation = new Transformation().x(1).y(2).radius(3).gravity("center").quality(0.4).prefix("a"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "g_center,p_a,q_0.4,r_3,x_1,y_2/test", result); + } + + @Test + @TestCaseName("{method}: {params}") + @Parameters + public void testQuality(Object quality, String result) { + Transformation transformation = new Transformation().quality(quality); + assertEquals(result, transformation.generate()); + } + + @SuppressWarnings("unused") + private Object[][] parametersForTestQuality() { + return new Object[][]{ + {0.4, "q_0.4"}, + {"0.4", "q_0.4"}, + {"auto", "q_auto"}, + {"auto:good", "q_auto:good"}}; + + } + + @Test + @TestCaseName("{method}: {0}") + @Parameters + public void testAutoGravity(String value, String serialized) { + Transformation transformation = new Transformation().crop("crop").gravity(value).width(0.5f); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "c_crop," + serialized + ",w_0.5/test", result); + } + + @SuppressWarnings("unused") + private String[][] parametersForTestAutoGravity() { + return new String[][]{ + {"west", "g_west"}, + {"auto", "g_auto"}, + {"auto:good", "g_auto:good"}, + {"auto:ocr_text", "g_auto:ocr_text"}, + {"ocr_text", "g_ocr_text"}, + {"ocr_text:adv_ocr", "g_ocr_text:adv_ocr"} + }; + + } + + @Test + public void testTransformationSimple() { + // should support named transformation + Transformation transformation = new Transformation().named("blip"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "t_blip/test", result); + } + + @Test + public void testTransformationArray() { + // should support array of named transformations + Transformation transformation = new Transformation().named("blip", "blop"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "t_blip.blop/test", result); + } + + @Test + public void testNamedTransformationWithSpaces() { + // should support named transformations with spaces + Transformation transformation = new Transformation().named("blip blop"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "t_blip%20blop/test", result); + } + + @Test + public void testBaseTransformations() { + // should support base transformation + Transformation transformation = new Transformation().x(100).y(100).crop("fill").chain().crop("crop").width(100); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals("100", transformation.getHtmlWidth()); + assertEquals(DEFAULT_UPLOAD_PATH + "c_fill,x_100,y_100/c_crop,w_100/test", result); + } + + @Test + public void testBaseTransformationArray() { + // should support array of base transformations + Transformation transformation = new Transformation().x(100).y(100).width(200).crop("fill").chain().radius(10).chain().crop("crop").width(100); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals("100", transformation.getHtmlWidth()); + assertEquals(DEFAULT_UPLOAD_PATH + "c_fill,w_200,x_100,y_100/r_10/c_crop,w_100/test", result); + } + + @Test + public void testNoEmptyTransformation() { + // should not include empty transformations + Transformation transformation = new Transformation().chain().x(100).y(100).crop("fill").chain(); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "c_fill,x_100,y_100/test", result); + } + + @Test + public void testHttpEscape() { + // should escape http urls + String result = cloudinary.url().type("youtube").generate("http://www.youtube.com/watch?v=d9NF2edxy-M"); + assertEquals("https://res.cloudinary.com/test123/image/youtube/http://www.youtube.com/watch%3Fv%3Dd9NF2edxy-M", result); + } + + @Test + public void testBackground() { + // should support background + Transformation transformation = new Transformation().background("red"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "b_red/test", result); + transformation = new Transformation().background("#112233"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "b_rgb:112233/test", result); + } + + @Test + public void testDefaultImage() { + // should support default_image + Transformation transformation = new Transformation().defaultImage("default"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "d_default/test", result); + } + + @Test + public void testAngle() { + // should support angle + Transformation transformation = new Transformation().angle(12); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "a_12/test", result); + transformation = new Transformation().angle("exif", "12"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "a_exif.12/test", result); + } + + @Test + public void testFetchFormat() { + // should support format for fetch urls + String result = cloudinary.url().format("jpg").type("fetch").generate("http://cloudinary.com/images/old_logo.png"); + assertEquals("https://res.cloudinary.com/test123/image/fetch/f_jpg/http://cloudinary.com/images/old_logo.png", result); + } + + @Test + public void testUseFetchFormat() { + // should support use fetch format, adds the format but not an extension + String result = cloudinary.url().format("jpg").useFetchFormat(true).generate("old_logo"); + assertEquals("https://res.cloudinary.com/test123/image/upload/f_jpg/old_logo", result); + } + + @Test + public void testEffect() { + // should support effect + Transformation transformation = new Transformation().effect("sepia"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "e_sepia/test", result); + } + + @Test + public void testEffectWithParam() { + // should support effect with param + Transformation transformation = new Transformation().effect("sepia", 10); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "e_sepia:10/test", result); + } + + @Test + public void testArtisticFilter() { + Transformation transformation = new Transformation().effect("art", "incognito"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "e_art:incognito/test", result); + } + + @Test + public void testDensity() { + // should support density + Transformation transformation = new Transformation().density(150); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "dn_150/test", result); + } + + @Test + public void testPage() { + // should support page + Transformation transformation = new Transformation().page(5); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "pg_5/test", result); + } + + @Test + public void testBorder() { + // should support border + Transformation transformation = new Transformation().border(5, "black"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "bo_5px_solid_black/test", result); + transformation = new Transformation().border(5, "#ffaabbdd"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "bo_5px_solid_rgb:ffaabbdd/test", result); + transformation = new Transformation().border("1px_solid_blue"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "bo_1px_solid_blue/test", result); + } + + @Test + public void testFlags() { + // should support flags + Transformation transformation = new Transformation().flags("abc"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "fl_abc/test", result); + transformation = new Transformation().flags("abc", "def"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "fl_abc.def/test", result); + } + + @Test + public void testOpacity() { + // should support opacity + Transformation transformation = new Transformation().opacity(50); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "o_50/test", result); + + transformation = new Transformation().opacity("$var"); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "o_$var/test", result); + } + + @SuppressWarnings("unchecked") + @Test + public void testImageTag() { + Transformation transformation = new Transformation().width(100).height(101).crop("crop"); + String result = cloudinary.url().transformation(transformation).imageTag("test", asMap("alt", "my image")); + assertEquals("my image", result); + transformation = new Transformation().width(0.9).height(0.9).crop("crop").responsiveWidth(true); + result = cloudinary.url().transformation(transformation).imageTag("test", asMap("alt", "my image")); + assertEquals( + "my image", + result); + result = cloudinary.url().transformation(transformation).imageTag("test", asMap("alt", "my image", "class", "extra")); + assertEquals( + "my image", + result); + transformation = new Transformation().width("auto").crop("crop"); + result = cloudinary.url().transformation(transformation).imageTag("test", asMap("alt", "my image", "responsive_placeholder", "blank")); + assertEquals( + "my image", + result); + result = cloudinary.url().transformation(transformation).imageTag("test", asMap("alt", "my image", "responsive_placeholder", "other.gif")); + assertEquals( + "my image", + result); + } + + @Test + public void testClientHints() { + String testTag; + String message = "should not implement responsive behaviour if client hints is true"; + cloudinary.config.clientHints = true; + Transformation trans = new Transformation() + .crop("scale") + .width("auto") + .dpr("auto"); + testTag = cloudinary.url().transformation(trans).imageTag("sample.jpg"); + assertTrue(testTag.startsWith("singletonMap("expires_at", inTwentyMinutes)); + URI uri = new URI(url); + Map parameters = getUrlParameters(uri); + assertEquals("imgÿ=&é", parameters.get("public_id")); + assertEquals("jpg", parameters.get("format")); + assertEquals("a", parameters.get("api_key")); + assertEquals(String.valueOf(inTwentyMinutes), parameters.get("expires_at")); + assertEquals("/v1_1/test123/image/download", uri.getPath()); + } + + @SuppressWarnings("unchecked") + @Test + public void testZipDownload() throws Exception { + String url = cloudinary.zipDownload("ttag", emptyMap()); + URI uri = new URI(url); + Map parameters = getUrlParameters(uri); + assertEquals("ttag", parameters.get("tag")); + assertEquals("a", parameters.get("api_key")); + assertEquals("/v1_1/test123/image/download_tag.zip", uri.getPath()); + } + + @Test + public void testDownloadSprite() throws Exception{ + final String spriteTestTag = "sprite_tag"; + final String url1 = "https://res.cloudinary.com/demo/image/upload/sample"; + final String url2 = "https://res.cloudinary.com/demo/image/upload/car"; + + String urlFromTag = cloudinary.downloadGeneratedSprite(spriteTestTag, null); + String urlFromUrls = cloudinary.downloadGeneratedSprite(new String[]{url1, url2}, null); + + assertTrue(urlFromTag.startsWith("https://api.cloudinary.com/v1_1/" + cloudinary.config.cloudName + "/image/sprite?mode=download")); + assertTrue(urlFromUrls.startsWith("https://api.cloudinary.com/v1_1/" + cloudinary.config.cloudName + "/image/sprite?mode=download")); + assertTrue(urlFromUrls.contains("urls[]=" + URLEncoder.encode(url1, "UTF-8"))); + assertTrue(urlFromUrls.contains("urls[]=" + URLEncoder.encode(url2, "UTF-8"))); + + Map parameters = getUrlParameters(new URI(urlFromTag)); + assertEquals(spriteTestTag, parameters.get("tag")); + assertNotNull(parameters.get("timestamp")); + assertNotNull(parameters.get("signature")); + + parameters = getUrlParameters(new URI(urlFromUrls)); + assertNotNull(parameters.get("timestamp")); + assertNotNull(parameters.get("signature")); + } + + @Test + public void testDownloadMulti() throws Exception{ + cloudinary = new Cloudinary("cloudinary://571927874334573:yABWqlfSV2d5pRW4ujHJYA7SD34@nitzanj?load_strategies=false"); + + final String multiTestTag = "multi_test_tag"; + final String url1 = "https://res.cloudinary.com/demo/image/upload/sample"; + final String url2 = "https://res.cloudinary.com/demo/image/upload/car"; + + String urlFromTag = cloudinary.downloadMulti(multiTestTag, null); + String urlFromUrls = cloudinary.downloadMulti(new String[]{url1, url2}, null); + + assertTrue(urlFromTag.startsWith("https://api.cloudinary.com/v1_1/" + cloudinary.config.cloudName + "/image/multi?mode=download")); + assertTrue(urlFromUrls.startsWith("https://api.cloudinary.com/v1_1/" + cloudinary.config.cloudName + "/image/multi?mode=download")); + assertTrue(urlFromUrls.contains("urls[]=" + URLEncoder.encode(url1, "UTF-8"))); + assertTrue(urlFromUrls.contains("urls[]=" + URLEncoder.encode(url2, "UTF-8"))); + + Map parameters = getUrlParameters(new URI(urlFromTag)); + assertEquals(multiTestTag, parameters.get("tag")); + assertNotNull(parameters.get("timestamp")); + assertNotNull(parameters.get("signature")); + + parameters = getUrlParameters(new URI(urlFromUrls)); + assertNotNull(parameters.get("timestamp")); + assertNotNull(parameters.get("signature")); + + } + + @Test + public void testDownloadFolderShouldReturnURLWithResourceTypeAllByDefault() throws UnsupportedEncodingException { + String url = cloudinary.downloadFolder("folder", null); + assertTrue(url.contains("all")); + } + + @Test + public void testDownloadFolderShouldAllowToOverrideResourceType() throws UnsupportedEncodingException { + String url = cloudinary.downloadFolder("folder", Collections.singletonMap("resource_type", "audio")); + assertTrue(url.contains("audio")); + } + + @Test + public void testDownloadFolderShouldPutFolderPathAsPrefixes() throws UnsupportedEncodingException { + String url = cloudinary.downloadFolder("folder", null); + assertTrue(url.contains("prefixes[]=folder")); + } + + @Test + public void testDownloadFolderShouldIncludeSpecifiedTargetFormat() throws UnsupportedEncodingException { + String url = cloudinary.downloadFolder("folder", Collections.singletonMap("target_format", "rar")); + assertTrue(url.contains("target_format=rar")); + } + + @Test + public void testDownloadFolderShouldNotIncludeTargetFormatIfNotSpecified() throws UnsupportedEncodingException { + String url = cloudinary.downloadFolder("folder", null); + assertFalse(url.contains("target_format")); + } + + @Test + public void testSpriteCss() { + String result = cloudinary.url().generateSpriteCss("test"); + assertEquals("https://res.cloudinary.com/test123/image/sprite/test.css", result); + result = cloudinary.url().generateSpriteCss("test.css"); + assertEquals("https://res.cloudinary.com/test123/image/sprite/test.css", result); + } + + @SuppressWarnings("unchecked") + @Test + public void testEscapePublicId() { + // should escape public_ids + Map tests = asMap("a b", "a%20b", "a+b", "a%2Bb", "a%20b", "a%20b", "a-b", "a-b", "a??b", "a%3F%3Fb"); + for (Map.Entry entry : tests.entrySet()) { + String result = cloudinary.url().generate(entry.getKey()); + assertEquals(DEFAULT_UPLOAD_PATH + "" + entry.getValue(), result); + } + } + + @Test + public void testSignedUrl() { + // should correctly sign a url + String expected = DEFAULT_UPLOAD_PATH + "s--Ai4Znfl3--/c_crop,h_20,w_10/v1234/image.jpg"; + String actual = cloudinary.url().version(1234).transformation(new Transformation().crop("crop").width(10).height(20)).signed(true) + .generate("image.jpg"); + assertEquals(expected, actual); + + expected = DEFAULT_UPLOAD_PATH + "s----SjmNDA--/v1234/image.jpg"; + actual = cloudinary.url().version(1234).signed(true).generate("image.jpg"); + assertEquals(expected, actual); + + expected = DEFAULT_UPLOAD_PATH + "s--Ai4Znfl3--/c_crop,h_20,w_10/image.jpg"; + actual = cloudinary.url().transformation(new Transformation().crop("crop").width(10).height(20)).signed(true).generate("image.jpg"); + assertEquals(expected, actual); + } + + @Test + public void testSignedUrlSHA256() { + cloudinary.config.signatureAlgorithm = SignatureAlgorithm.SHA256; + + String url = cloudinary.url().signed(true).generate("sample.jpg"); + assertEquals(DEFAULT_UPLOAD_PATH + "s--2hbrSMPO--/sample.jpg", url); + } + + @Test + public void testResponsiveWidth() { + // should support responsive width + Transformation trans = new Transformation().width(100).height(100).crop("crop").responsiveWidth(true); + String result = cloudinary.url().transformation(trans).generate("test"); + assertTrue(trans.isResponsive()); + assertEquals(DEFAULT_UPLOAD_PATH + "c_crop,h_100,w_100/c_limit,w_auto/test", result); + Transformation.setResponsiveWidthTransformation(asMap("width", "auto", "crop", "pad")); + trans = new Transformation().width(100).height(100).crop("crop").responsiveWidth(true); + result = cloudinary.url().transformation(trans).generate("test"); + assertTrue(trans.isResponsive()); + assertEquals(DEFAULT_UPLOAD_PATH + "c_crop,h_100,w_100/c_pad,w_auto/test", result); + Transformation.setResponsiveWidthTransformation(null); + } + + @Parameters({ + "auto:20|c_fill\\,w_auto:20", + "auto:20:350|c_fill\\,w_auto:20:350", + "auto:breakpoints|c_fill\\,w_auto:breakpoints", + "auto:breakpoints_100_1900_20_15|c_fill\\,w_auto:breakpoints_100_1900_20_15", + "auto:breakpoints:json|c_fill\\,w_auto:breakpoints:json"}) + @TestCaseName("Width {0}: {1}") + @Test + public void testShouldSupportAutoWidth(String width, String result) { + String trans; + trans = new Transformation().width(width).crop("fill").generate(); + assertEquals(result, trans); + } + + @Test + public void testEagerWithStreamingProfile() throws IOException { + Transformation transformation = new EagerTransformation().format("m3u8").streamingProfile("full_hd"); + assertEquals("sp_full_hd/m3u8", transformation.generate()); + } + + @Test + public void testEagerWithChaining() throws IOException { + Transformation transformation = new EagerTransformation().angle(13).chain().effect("sepia").chain().format("webp"); + assertEquals("a_13/e_sepia/webp", transformation.generate()); + } + + @Test + public void testShouldSupportIhIw() { + String trans = new Transformation().width("iw").height("ih").crop("crop").generate(); + assertEquals("c_crop,h_ih,w_iw", trans); + } + + @Test + public void testVideoCodec() { + // should support a string value + String actual = cloudinary.url().resourceType("video").transformation(new Transformation().videoCodec("auto")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vc_auto/video_id", actual); + // should support a hash value + actual = cloudinary.url().resourceType("video") + .transformation( + new Transformation().videoCodec(asMap("codec", "h264", "profile", "basic", "level", "3.1")) + ).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vc_h264:basic:3.1/video_id", actual); + } + + @Test + public void testVideoCodecBFrameTrue() { + String actual = cloudinary.url().resourceType("video") + .transformation( + new Transformation().videoCodec(asMap("codec", "h264", "profile", "basic", "level", "3.1", "b_frames", "true")) + ).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vc_h264:basic:3.1/video_id", actual); + } + + @Test + public void testVideoCodecBFrameFalse() { + String actual = cloudinary.url().resourceType("video") + .transformation( + new Transformation().videoCodec(asMap("codec", "h264", "profile", "basic", "level", "3.1", "b_frames", "false")) + ).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vc_h264:basic:3.1:bframes_no/video_id", actual); + } + + @Test + public void testAudioCodec() { + // should support a string value + String actual = cloudinary.url().resourceType("video").transformation(new Transformation().audioCodec("acc")).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "ac_acc/video_id", actual); + } + + @Test + public void testBitRate() { + // should support a numeric value + String actual = cloudinary.url().resourceType("video").transformation(new Transformation().bitRate(2048)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "br_2048/video_id", actual); + // should support a string value + actual = cloudinary.url().resourceType("video").transformation(new Transformation().bitRate("44k")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "br_44k/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().bitRate("1m")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "br_1m/video_id", actual); + + } + + @Test + public void testAudioFrequency() { + // should support an integer value + String actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().audioFrequency(44100)).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "af_44100/video_id", actual); + // should support a string value + actual = cloudinary.url().resourceType("video").transformation(new Transformation().audioFrequency("44100")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "af_44100/video_id", actual); + } + + @Test + public void testVideoSampling() { + String actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().videoSamplingFrames(20)).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vs_20/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().videoSamplingSeconds(20)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vs_20s/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().videoSamplingSeconds(20.0)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vs_20.0s/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().videoSampling("2.3s")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "vs_2.3s/video_id", actual); + } + + @Test + public void testStartOffset() { + String actual = cloudinary.url().resourceType("video").transformation(new Transformation().startOffset(2.63)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "so_2.63/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().startOffset("2.63p")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "so_2.63p/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().startOffset("2.63%")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "so_2.63p/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().startOffsetPercent(2.63)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "so_2.63p/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().startOffset("auto")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "so_auto/video_id", actual); + } + + @Test + public void testDuration() { + String actual = cloudinary.url().resourceType("video").transformation(new Transformation().duration(2.63)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "du_2.63/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().duration("2.63p")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "du_2.63p/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().duration("2.63%")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "du_2.63p/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().durationPercent(2.63)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "du_2.63p/video_id", actual); + } + + @Test + public void testOffset() { + + String actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().offset("2.66..3.21")).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "eo_3.21,so_2.66/video_id", actual); + actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().offset(new float[]{2.67f, 3.22f})).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "eo_3.22,so_2.67/video_id", actual); + actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().offset(new double[]{2.67, 3.22})).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "eo_3.22,so_2.67/video_id", actual); + actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().offset(new String[]{"35%", "70%"})).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "eo_70p,so_35p/video_id", actual); + actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().offset(new String[]{"36p", "71p"})).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "eo_71p,so_36p/video_id", actual); + actual = cloudinary.url().resourceType("video") + .transformation(new Transformation().offset(new String[]{"35.5p", "70.5p"})).generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "eo_70.5p,so_35.5p/video_id", actual); + + } + + @Test + public void testZoom() { + String actual = cloudinary.url().resourceType("video").transformation(new Transformation().zoom("1.5")) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "z_1.5/video_id", actual); + actual = cloudinary.url().resourceType("video").transformation(new Transformation().zoom(1.5)) + .generate("video_id"); + assertEquals(VIDEO_UPLOAD_PATH + "z_1.5/video_id", actual); + } + + @Test + public void testUtils() { + assertEquals(ObjectUtils.asBoolean(true, null), true); + assertEquals(ObjectUtils.asBoolean(false, null), false); + } + + @Test + public void testVideoTag() { + String expectedUrl = VIDEO_UPLOAD_PATH + "movie"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl, expectedUrl); + assertEquals(expectedTag, cloudinary.url().videoTag("movie", emptyMap())); + assertEquals(expectedTag, cloudinary.url().publicId("movie").videoTag()); + assertEquals(expectedTag, cloudinary.url().videoTag("movie")); + } + + @Test + public void testVideoTagWithAttributes() { + Map attributes = asMap( + "autoplay", true, + "controls", null, + "loop", null, + "muted", "true", + "preload", null, + "style", "border: 1px"); + String expectedUrl = VIDEO_UPLOAD_PATH + "movie"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl, expectedUrl); + assertEquals(expectedTag, cloudinary.url().videoTag("movie", attributes)); + } + + @Test + public void testVideoTagWithTransformation() { + Transformation transformation = new Transformation().videoCodec(asMap("codec", "h264")) + .audioCodec("acc").startOffset(3); + String expectedUrl = VIDEO_UPLOAD_PATH + "ac_acc,so_3.0,vc_h264/movie"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl); + String actualTag = cloudinary.url().transformation(transformation).sourceTypes(new String[]{"mp4"}) + .videoTag("movie", asMap("html_height", "100", "html_width", "200")); + assertEquals(expectedTag, actualTag); + + expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl, expectedUrl); + actualTag = cloudinary.url().transformation(transformation) + .videoTag("movie", asMap("html_height", "100", "html_width", "200")); + assertEquals(expectedTag, actualTag); + + transformation.width(250); + expectedUrl = VIDEO_UPLOAD_PATH + "ac_acc,so_3.0,vc_h264,w_250/movie"; + expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl, expectedUrl); + actualTag = cloudinary.url().transformation(transformation) + .videoTag("movie", asMap()); + assertEquals(expectedTag, actualTag); + + transformation.crop("fit"); + expectedUrl = VIDEO_UPLOAD_PATH + "ac_acc,c_fit,so_3.0,vc_h264,w_250/movie"; + expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl, expectedUrl); + actualTag = cloudinary.url().transformation(transformation) + .videoTag("movie", asMap()); + assertEquals(expectedTag, actualTag); + } + + @Test + public void testVideoTagWithFallback() { + String expectedUrl = VIDEO_UPLOAD_PATH + "movie"; + String fallback = "Cannot display video"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, fallback); + String actualTag = cloudinary.url().fallbackContent(fallback).sourceTypes(new String[]{"mp4"}) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + + expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl, expectedUrl, fallback); + actualTag = cloudinary.url().fallbackContent(fallback).videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + } + + @Test + public void testVideoTagWithSourceTypes() { + String expectedUrl = VIDEO_UPLOAD_PATH + "movie"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedUrl); + String actualTag = cloudinary.url().sourceTypes(new String[]{"ogv", "mp4"}) + .videoTag("movie.mp4", emptyMap()); + assertEquals(expectedTag, actualTag); + } + + @Test + public void testVideoTagWithSourceTransformation() { + String expectedUrl = VIDEO_UPLOAD_PATH + "q_50/w_100/movie"; + String expectedOgvUrl = VIDEO_UPLOAD_PATH + "q_50/w_100/q_70/movie"; + String expectedMp4Url = VIDEO_UPLOAD_PATH + "q_50/w_100/q_30/movie"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedMp4Url, expectedOgvUrl); + String actualTag = cloudinary.url().transformation(new Transformation().quality(50).chain().width(100)) + .sourceTransformationFor("mp4", new Transformation().quality(30)) + .sourceTransformationFor("ogv", new Transformation().quality(70)) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + + expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl, expectedUrl, expectedMp4Url); + actualTag = cloudinary.url().transformation(new Transformation().quality(50).chain().width(100)) + .sourceTransformationFor("mp4", new Transformation().quality(30)) + .sourceTransformationFor("ogv", new Transformation().quality(70)) + .sourceTypes(new String[]{"webm", "mp4"}).videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + } + + @Test + public void testVideoTagWithPoster() { + String expectedUrl = VIDEO_UPLOAD_PATH + "movie"; + String posterUrl = "http://image/somewhere.jpg"; + String expectedTag = ""; + expectedTag = String.format(expectedTag, posterUrl, expectedUrl); + String actualTag = cloudinary.url().sourceTypes(new String[]{"mp4"}).poster(posterUrl) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + + posterUrl = VIDEO_UPLOAD_PATH + "g_north/movie.jpg"; + expectedTag = ""; + expectedTag = String.format(expectedTag, posterUrl, expectedUrl); + actualTag = cloudinary.url().sourceTypes(new String[]{"mp4"}) + .poster(new Transformation().gravity("north")) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + + posterUrl = DEFAULT_UPLOAD_PATH + "g_north/my_poster.jpg"; + expectedTag = ""; + expectedTag = String.format(expectedTag, posterUrl, expectedUrl); + actualTag = cloudinary.url().sourceTypes(new String[]{"mp4"}) + .poster(cloudinary.url() + .publicId("my_poster") + .format("jpg") + .transformation(new Transformation().gravity("north"))) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + + expectedTag = ""; + expectedTag = String.format(expectedTag, expectedUrl); + actualTag = cloudinary.url().sourceTypes(new String[]{"mp4"}) + .poster(null) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + + actualTag = cloudinary.url().sourceTypes(new String[]{"mp4"}) + .poster(false) + .videoTag("movie", emptyMap()); + assertEquals(expectedTag, actualTag); + } + + @Test + public void videoTagWithAuthTokenTest() { + String actualTag = cloudinary.url().transformation(new Transformation()) + .type("upload") + .authToken(new AuthToken("123456").duration(300)) + .signed(true) + .secure(true) + .videoTag("sample", ObjectUtils.asMap( + "controls", true, + "loop", true) + ); + assert(actualTag.contains("cld_token")); + } + + @Test + public void testAspectRatio() { + String actual = cloudinary.url().transformation(new Transformation().aspectRatio("1.5")) + .generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "ar_1.5/test", actual); + actual = cloudinary.url().transformation(new Transformation().aspectRatio(1.5)) + .generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "ar_1.5/test", actual); + actual = cloudinary.url().transformation(new Transformation().aspectRatio(3, 2)) + .generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "ar_3:2/test", actual); + } + + @Test + public void testOverlayOptions() { + Object tests[] = { + new Layer().publicId("logo"), + "logo", + new Layer().publicId("folder/logo"), + "folder:logo", + new Layer().publicId("logo").type("private"), + "private:logo", + new Layer().publicId("logo").format("png"), + "logo.png", + new Layer().resourceType("video").publicId("cat"), + "video:cat", + new TextLayer().text("Hello/World").fontFamily("Arial").fontSize(18), + "text:Arial_18:Hello%252FWorld", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18), + "text:Arial_18:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18) + .fontWeight("bold").fontStyle("italic").letterSpacing("4").lineSpacing(3), + "text:Arial_18_bold_italic_letter_spacing_4_line_spacing_3:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18) + .fontWeight("bold").fontStyle("italic").letterSpacing(4).lineSpacing(3), + "text:Arial_18_bold_italic_letter_spacing_4_line_spacing_3:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18) + .fontAntialiasing("best").fontHinting("medium"), + "text:Arial_18_antialias_best_hinting_medium:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new SubtitlesLayer().publicId("sample_sub_en.srt"), "subtitles:sample_sub_en.srt", + new SubtitlesLayer().publicId("sample_sub_he.srt").fontFamily("Arial").fontSize(40), + "subtitles:Arial_40:sample_sub_he.srt", + new FetchLayer().url("https://test").resourceType("image"), + "fetch:aHR0cHM6Ly90ZXN0", + new FetchLayer().url("https://test"), + "fetch:aHR0cHM6Ly90ZXN0", + new FetchLayer().url("https://test").resourceType("video"), + "video:fetch:aHR0cHM6Ly90ZXN0", + new FetchLayer().url("https://www.test.com/test/JE01118-YGP900_1_lar.jpg?version=432023"), + "fetch:aHR0cHM6Ly93d3cudGVzdC5jb20vdGVzdC9KRTAxMTE4LVlHUDkwMF8xX2xhci5qcGc_dmVyc2lvbj00MzIwMjM=" + }; + + for (int i = 0; i < tests.length; i += 2) { + Object layer = tests[i]; + String expected = (String) tests[i + 1]; + assertEquals(expected, layer.toString()); + } + } + + @Test + @SuppressWarnings("deprecation") + public void testBackwardCampatibleOverlayOptions() { + Object tests[] = { + new Layer().publicId("logo"), + "logo", + new Layer().publicId("folder/logo"), + "folder:logo", + new Layer().publicId("logo").type("private"), + "private:logo", + new Layer().publicId("logo").format("png"), + "logo.png", + new Layer().resourceType("video").publicId("cat"), + "video:cat", + new TextLayer().text("Hello/World").fontFamily("Arial").fontSize(18), + "text:Arial_18:Hello%252FWorld", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18), + "text:Arial_18:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18) + .fontWeight("bold").fontStyle("italic").letterSpacing("4"), + "text:Arial_18_bold_italic_letter_spacing_4:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new SubtitlesLayer().publicId("sample_sub_en.srt"), "subtitles:sample_sub_en.srt", + new SubtitlesLayer().publicId("sample_sub_he.srt").fontFamily("Arial").fontSize(40), + "subtitles:Arial_40:sample_sub_he.srt"}; + + for (int i = 0; i < tests.length; i += 2) { + Object layer = tests[i]; + String expected = (String) tests[i + 1]; + assertEquals(expected, layer.toString()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testOverlayError1() { + // Must supply font_family for text in overlay + cloudinary.url().transformation(new Transformation().overlay(new TextLayer().fontStyle("italic"))).generate("test"); + } + + @Test(expected = IllegalArgumentException.class) + public void testOverlayError2() { + // Must supply public_id for for non-text underlay + cloudinary.url().transformation(new Transformation().underlay(new Layer().resourceType("video"))).generate("test"); + } + + @Test + public void testResponsiveBreakpointsToJson() { + assertEquals("an empty ResponsiveBreakpoint should have create_derived=true", + "{\"create_derived\":true}", + new ResponsiveBreakpoint().toString() + ); + String[] expectedArr = "{\"create_derived\":false,\"max_width\":500,\"min_width\":100,\"max_images\":5,\"transformation\":\"a_45\"}".split("[{}]")[1].split(",(?=\")"); + Arrays.sort(expectedArr); + JSONObject actual = new ResponsiveBreakpoint().createDerived(false) + .transformation(new Transformation().angle(45)) + .maxWidth(500) + .minWidth(100) + .maxImages(5); + String[] actualArr = actual.toString().split("[{}]")[1].split(",(?=\")"); + Arrays.sort(actualArr); + assertArrayEquals(expectedArr, actualArr); + } + + @Test + public void testFps() { + Transformation t = new Transformation().fps("24-29.97"); + assertEquals("fps_24-29.97", t.generate()); + t = new Transformation().fps(24); + assertEquals("fps_24", t.generate()); + t = new Transformation().fps(24.5); + assertEquals("fps_24.5", t.generate()); + t = new Transformation().fps("24"); + assertEquals("fps_24", t.generate()); + t = new Transformation().fps("-24"); + assertEquals("fps_-24", t.generate()); + t = new Transformation().fps(24, 29.97); + assertEquals("fps_24-29.97", t.generate()); + t = new Transformation().fps(24, null); + assertEquals("fps_24-", t.generate()); + t = new Transformation().fps(null, 29.97); + assertEquals("fps_-29.97", t.generate()); + } + + @Test + public void testKeyframeInterval() { + assertEquals("ki_10.0", new Transformation().keyframeInterval(10).generate()); + assertEquals("ki_0.05", new Transformation().keyframeInterval(0.05f).generate()); + assertEquals("ki_3.45", new Transformation().keyframeInterval(3.45f).generate()); + assertEquals("ki_300.0", new Transformation().keyframeInterval(300).generate()); + assertEquals("ki_10", new Transformation().keyframeInterval("10").generate()); + assertEquals("", new Transformation().keyframeInterval("").generate()); + assertEquals("", new Transformation().keyframeInterval(null).generate()); + } + + @Test + public void testCustomFunction() { + assertEquals("fn_wasm:blur_wasm", new Transformation().customFunction(wasm("blur_wasm")).generate()); + assertEquals("fn_remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg==", + new Transformation().customFunction(remote("https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction")).generate()); + } + + @Test + public void testCustomPreFunction() { + assertEquals("fn_pre:wasm:blur_wasm", new Transformation().customPreFunction(wasm("blur_wasm")).generate()); + assertEquals("fn_pre:remote:aHR0cHM6Ly9kZjM0cmE0YS5leGVjdXRlLWFwaS51cy13ZXN0LTIuYW1hem9uYXdzLmNvbS9kZWZhdWx0L2Nsb3VkaW5hcnlGdW5jdGlvbg==", + new Transformation().customPreFunction(remote("https://df34ra4a.execute-api.us-west-2.amazonaws.com/default/cloudinaryFunction")).generate()); + } + + public static Map getUrlParameters(URI uri) throws UnsupportedEncodingException { + Map params = new HashMap(); + for (String param : uri.getRawQuery().split("&")) { + String pair[] = param.split("="); + String key = URLDecoder.decode(pair[0], "UTF-8"); + String value = ""; + if (pair.length > 1) { + value = URLDecoder.decode(pair[1], "UTF-8"); + } + params.put(key, value); + } + return params; + } + + @Test + public void testUrlCloneConfig() { + // verify that secure (from url.config) is cloned as well: + Url url = cloudinary.url().cloudName("cloud").format("frmt").publicId("123"); + assertEquals("https://res.cloudinary.com/cloud/image/upload/123.frmt", url.clone().generate()); + } + + @Test + public void testConfiguration() throws IllegalAccessException { + Configuration config = new Configuration(); + randomizeFields(config); + Map map = config.asMap(); + Configuration copy = new Configuration(map); + assertFieldsEqual(config, copy); + + copy = new Configuration(config); + assertFieldsEqual(config, copy); + } + + @Test + public void testCloudinaryUrlValidScheme() { + String cloudinaryUrl = "cloudinary://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test"; + Configuration.from(cloudinaryUrl); + } + + @Test(expected = IllegalArgumentException.class) + public void testCloudinaryUrlInvalidScheme() { + String cloudinaryUrl = "https://123456789012345:ALKJdjklLJAjhkKJ45hBK92baj3@test"; + Configuration.from(cloudinaryUrl); + } + + @Test(expected = IllegalArgumentException.class) + public void testCloudinaryUrlEmptyScheme() { + String cloudinaryUrl = " "; + Configuration.from(cloudinaryUrl); + } + + @Test + public void testApiSignRequestSHA1() { + cloudinary.config.signatureAlgorithm = SignatureAlgorithm.SHA1; + String signature = cloudinary.apiSignRequest(ObjectUtils.asMap("cloud_name", "dn6ot3ged", "timestamp", 1568810420, "username", "user@cloudinary.com"), "hdcixPpR2iKERPwqvH6sHdK9cyac", cloudinary.config.signatureVersion); + assertEquals("14c00ba6d0dfdedbc86b316847d95b9e6cd46d94", signature); + } + + @Test + public void testApiSignRequestSHA256() { + cloudinary.config.signatureAlgorithm = SignatureAlgorithm.SHA256; + String signature = cloudinary.apiSignRequest(ObjectUtils.asMap("cloud_name", "dn6ot3ged", "timestamp", 1568810420, "username", "user@cloudinary.com"), "hdcixPpR2iKERPwqvH6sHdK9cyac", cloudinary.config.signatureVersion); + assertEquals("45ddaa4fa01f0c2826f32f669d2e4514faf275fe6df053f1a150e7beae58a3bd", signature); + } + + @Test + public void testDownloadBackedupAsset() throws UnsupportedEncodingException, URISyntaxException { + String url = cloudinary.downloadBackedupAsset("62c2a18d622be7e190d21df8e05b1416", + "26fe6d95df856f6ae12f5678be94516a", ObjectUtils.emptyMap()); + + URI uri = new URI(url); + assertTrue(uri.getPath().endsWith("download_backup")); + + Map params = getUrlParameters(uri); + assertEquals("62c2a18d622be7e190d21df8e05b1416", params.get("asset_id")); + assertEquals("26fe6d95df856f6ae12f5678be94516a", params.get("version_id")); + assertNotNull(params.get("signature")); + assertNotNull(params.get("timestamp")); + } + + @Test + public void testRegisterUploaderStrategy() { + String className = "myUploadStrategy"; + Cloudinary.registerUploaderStrategy(className); + assertEquals(className, Cloudinary.UPLOAD_STRATEGIES.get(0)); + } + + @Test + public void testRegisterApiStrategy() { + String className = "myApiStrategy"; + Cloudinary.registerAPIStrategy(className); + assertEquals(className, Cloudinary.API_STRATEGIES.get(0)); + } + + private void assertFieldsEqual(Object a, Object b) throws IllegalAccessException { + assertEquals("Two objects must be the same class", a.getClass(), b.getClass()); + Field[] fields = a.getClass().getFields(); + for (Field field : fields) { + assertEquals("Field " + field.getName() + " should have equal values", field.get(a), field.get(b)); + } + } + + private void randomizeFields(Object instance) throws IllegalAccessException { + Random rand = new Random(); + Field[] fields = instance.getClass().getDeclaredFields(); + for (Field field : fields) { + setRandomValue(rand, field, instance); + } + } + + private void setRandomValue(Random rand, Field field, Object instance) throws IllegalAccessException { + field.setAccessible(true); + Type fieldType = field.getGenericType(); + if (Modifier.isFinal(field.getModifiers()) || Modifier.isStatic(field.getModifiers())) { + return; + } + + if (fieldType.equals(boolean.class) || fieldType.equals(Boolean.class)) { + field.set(instance, rand.nextBoolean()); + } else if (fieldType.equals(int.class) || fieldType.equals(Integer.class)) { + field.set(instance, rand.nextInt()); + } else if (fieldType.equals(long.class) || fieldType.equals(Long.class)) { + field.set(instance, rand.nextLong()); + } else if (field.get(instance) instanceof List) { + field.set(instance, Collections.singletonList(cloudinary.randomPublicId())); + } else if (fieldType.equals(String.class)) { + field.set(instance, cloudinary.randomPublicId()); + } else if (fieldType.equals(AuthToken.class)) { + AuthToken authToken = new AuthToken(); + randomizeFields(authToken); + field.set(instance, authToken); + } else if (field.get(instance) instanceof HashMap) { + Map map = new HashMap(); + map.put(cloudinary.randomPublicId(), rand.nextInt()); + field.set(instance, map); + } else if (fieldType instanceof Class && Enum.class.isAssignableFrom((Class) fieldType)) { + field.set(instance, randomEnum((Class) fieldType, rand)); + } else { + throw new IllegalArgumentException("Object have unexpected field type, randomizing not supported: " + field.getName() + ", type: " + field.getType().getSimpleName()); + } + } - private Cloudinary cloudinary; - - @Before - public void setUp() { - this.cloudinary = new Cloudinary("cloudinary://a:b@test123"); - } - - @Test - public void testCloudName() { - // should use cloud_name from config - String result = cloudinary.url().generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/test", result); - } - - @Test - public void testCloudNameOptions() { - // should allow overriding cloud_name in options - String result = cloudinary.url().cloudName("test321").generate("test"); - assertEquals("http://res.cloudinary.com/test321/image/upload/test", result); - } - - @Test - public void testSecureDistribution() { - // should use default secure distribution if secure=TRUE - String result = cloudinary.url().secure(true).generate("test"); - assertEquals("https://res.cloudinary.com/test123/image/upload/test", result); - } - - @Test - public void testSecureDistributionOverwrite() { - // should allow overwriting secure distribution if secure=TRUE - String result = cloudinary.url().secure(true).secureDistribution("something.else.com").generate("test"); - assertEquals("https://something.else.com/test123/image/upload/test", result); - } - - @Test - public void testSecureDistibution() { - // should take secure distribution from config if secure=TRUE - cloudinary.setConfig("secure_distribution", "config.secure.distribution.com"); - String result = cloudinary.url().secure(true).generate("test"); - assertEquals("https://config.secure.distribution.com/test123/image/upload/test", result); - } - - @Test - public void testSecureAkamai() { - // should default to akamai if secure is given with private_cdn and no - // secure_distribution - cloudinary.setConfig("secure", true); - cloudinary.setConfig("private_cdn", true); - String result = cloudinary.url().generate("test"); - assertEquals("https://test123-res.cloudinary.com/image/upload/test", result); - } - - @Test - public void testSecureNonAkamai() { - // should not add cloud_name if private_cdn and secure non akamai - // secure_distribution - cloudinary.setConfig("secure", true); - cloudinary.setConfig("private_cdn", true); - cloudinary.setConfig("secure_distribution", "something.cloudfront.net"); - String result = cloudinary.url().generate("test"); - assertEquals("https://something.cloudfront.net/image/upload/test", result); - } - - @Test - public void testHttpPrivateCdn() { - // should not add cloud_name if private_cdn and not secure - cloudinary.setConfig("private_cdn", true); - String result = cloudinary.url().generate("test"); - assertEquals("http://test123-res.cloudinary.com/image/upload/test", result); - } - - @Test - public void testFormat() { - // should use format from options - String result = cloudinary.url().format("jpg").generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/test.jpg", result); - } - - @Test - public void testCrop() { - Transformation transformation = new Transformation().width(100).height(101); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/h_101,w_100/test", result); - assertEquals("101", transformation.getHtmlHeight().toString()); - assertEquals("100", transformation.getHtmlWidth().toString()); - transformation = new Transformation().width(100).height(101).crop("crop"); - result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/c_crop,h_101,w_100/test", result); - } - - @Test - public void testVariousOptions() { - // should use x, y, radius, prefix, gravity and quality from options - Transformation transformation = new Transformation().x(1).y(2).radius(3).gravity("center").quality(0.4).prefix("a"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/g_center,p_a,q_0.4,r_3,x_1,y_2/test", result); - } - - @Test - public void testTransformationSimple() { - // should support named transformation - Transformation transformation = new Transformation().named("blip"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/t_blip/test", result); - } - - @Test - public void testTransformationArray() { - // should support array of named transformations - Transformation transformation = new Transformation().named("blip", "blop"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/t_blip.blop/test", result); - } - - @Test - public void testBaseTransformations() { - // should support base transformation - Transformation transformation = new Transformation().x(100).y(100).crop("fill").chain().crop("crop").width(100); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("100", transformation.getHtmlWidth().toString()); - assertEquals("http://res.cloudinary.com/test123/image/upload/c_fill,x_100,y_100/c_crop,w_100/test", result); - } - - @Test - public void testBaseTransformationArray() { - // should support array of base transformations - Transformation transformation = new Transformation().x(100).y(100).width(200).crop("fill").chain().radius(10).chain().crop("crop") - .width(100); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("100", transformation.getHtmlWidth().toString()); - assertEquals("http://res.cloudinary.com/test123/image/upload/c_fill,w_200,x_100,y_100/r_10/c_crop,w_100/test", result); - } - - @Test - public void testNoEmptyTransformation() { - // should not include empty transformations - Transformation transformation = new Transformation().chain().x(100).y(100).crop("fill").chain(); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/c_fill,x_100,y_100/test", result); - } - - @Test - public void testType() { - // should use type from options - String result = cloudinary.url().type("facebook").generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/facebook/test", result); - } - - @Test - public void testResourceType() { - // should use resource_type from options - String result = cloudinary.url().resourcType("raw").generate("test"); - assertEquals("http://res.cloudinary.com/test123/raw/upload/test", result); - } - - @Test - public void testIgnoreHttp() { - // should ignore http links only if type is not given or is asset - String result = cloudinary.url().generate("http://test"); - assertEquals("http://test", result); - result = cloudinary.url().type("asset").generate("http://test"); - assertEquals("http://test", result); - result = cloudinary.url().type("fetch").generate("http://test"); - assertEquals("http://res.cloudinary.com/test123/image/fetch/http://test", result); - } - - @Test - public void testFetch() { - // should escape fetch urls - String result = cloudinary.url().type("fetch").generate("http://blah.com/hello?a=b"); - assertEquals("http://res.cloudinary.com/test123/image/fetch/http://blah.com/hello%3Fa%3Db", result); - } - - @Test - public void testCname() { - // should support external cname - String result = cloudinary.url().cname("hello.com").generate("test"); - assertEquals("http://hello.com/test123/image/upload/test", result); - } - - @Test - public void testCnameSubdomain() { - // should support external cname with cdn_subdomain on - String result = cloudinary.url().cname("hello.com").cdnSubdomain(true).generate("test"); - assertEquals("http://a2.hello.com/test123/image/upload/test", result); - } - - @Test - public void testHttpEscape() { - // should escape http urls - String result = cloudinary.url().type("youtube").generate("http://www.youtube.com/watch?v=d9NF2edxy-M"); - assertEquals("http://res.cloudinary.com/test123/image/youtube/http://www.youtube.com/watch%3Fv%3Dd9NF2edxy-M", result); - } - - @Test - public void testBackground() { - // should support background - Transformation transformation = new Transformation().background("red"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/b_red/test", result); - transformation = new Transformation().background("#112233"); - result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/b_rgb:112233/test", result); - } - - @Test - public void testDefaultImage() { - // should support default_image - Transformation transformation = new Transformation().defaultImage("default"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/d_default/test", result); - } - - @Test - public void testAngle() { - // should support angle - Transformation transformation = new Transformation().angle(12); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/a_12/test", result); - transformation = new Transformation().angle("exif", "12"); - result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/a_exif.12/test", result); - } - - @Test - public void testOverlay() { - // should support overlay - Transformation transformation = new Transformation().overlay("text:hello"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/l_text:hello/test", result); - // should not pass width/height to html if overlay - transformation = new Transformation().overlay("text:hello").width(100).height(100); - result = cloudinary.url().transformation(transformation).generate("test"); - assertNull(transformation.getHtmlHeight()); - assertNull(transformation.getHtmlWidth()); - assertEquals("http://res.cloudinary.com/test123/image/upload/h_100,l_text:hello,w_100/test", result); - } - - @Test - public void testUnderlay() { - Transformation transformation = new Transformation().underlay("text:hello"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/u_text:hello/test", result); - // should not pass width/height to html if underlay - transformation = new Transformation().underlay("text:hello").width(100).height(100); - result = cloudinary.url().transformation(transformation).generate("test"); - assertNull(transformation.getHtmlHeight()); - assertNull(transformation.getHtmlWidth()); - assertEquals("http://res.cloudinary.com/test123/image/upload/h_100,u_text:hello,w_100/test", result); - } - - @Test - public void testFetchFormat() { - // should support format for fetch urls - String result = cloudinary.url().format("jpg").type("fetch").generate("http://cloudinary.com/images/logo.png"); - assertEquals("http://res.cloudinary.com/test123/image/fetch/f_jpg/http://cloudinary.com/images/logo.png", result); - } - - @Test - public void testEffect() { - // should support effect - Transformation transformation = new Transformation().effect("sepia"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/e_sepia/test", result); - } - - @Test - public void testEffectWithParam() { - // should support effect with param - Transformation transformation = new Transformation().effect("sepia", 10); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/e_sepia:10/test", result); - } - - @Test - public void testDensity() { - // should support density - Transformation transformation = new Transformation().density(150); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/dn_150/test", result); - } - - @Test - public void testPage() { - // should support page - Transformation transformation = new Transformation().page(5); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/pg_5/test", result); - } - - @Test - public void testBorder() { - // should support border - Transformation transformation = new Transformation().border(5, "black"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/bo_5px_solid_black/test", result); - transformation = new Transformation().border(5, "#ffaabbdd"); - result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/bo_5px_solid_rgb:ffaabbdd/test", result); - transformation = new Transformation().border("1px_solid_blue"); - result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/bo_1px_solid_blue/test", result); - } - - @Test - public void testFlags() { - // should support flags - Transformation transformation = new Transformation().flags("abc"); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/fl_abc/test", result); - transformation = new Transformation().flags("abc", "def"); - result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/fl_abc.def/test", result); - } - - @Test - public void testOpacity() { - // should support opacity - Transformation transformation = new Transformation().opacity(50); - String result = cloudinary.url().transformation(transformation).generate("test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/o_50/test", result); - } - - @Test - public void testImageTag() { - Transformation transformation = new Transformation().width(100).height(101).crop("crop"); - String result = cloudinary.url().transformation(transformation).imageTag("test", Cloudinary.asMap("alt", "my image")); - assertEquals( - "my image", - result); - transformation = new Transformation().width(0.9).height(0.9).crop("crop").responsiveWidth(true); - result = cloudinary.url().transformation(transformation).imageTag("test", Cloudinary.asMap("alt", "my image")); - assertEquals( - "my image", - result); - result = cloudinary.url().transformation(transformation).imageTag("test", Cloudinary.asMap("alt", "my image", "class", "extra")); - assertEquals( - "my image", - result); - transformation = new Transformation().width("auto").crop("crop"); - result = cloudinary.url().transformation(transformation).imageTag("test", Cloudinary.asMap("alt", "my image", "responsive_placeholder", "blank")); - assertEquals( - "my image", - result); - result = cloudinary.url().transformation(transformation).imageTag("test", Cloudinary.asMap("alt", "my image", "responsive_placeholder", "other.gif")); - assertEquals( - "my image", - result); - } - - @Test - public void testFolders() { - // should add version if public_id contains / - String result = cloudinary.url().generate("folder/test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/v1/folder/test", result); - result = cloudinary.url().version(123).generate("folder/test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/v123/folder/test", result); - } - - @Test - public void testFoldersWithVersion() { - // should not add version if public_id contains version already - String result = cloudinary.url().generate("v1234/test"); - assertEquals("http://res.cloudinary.com/test123/image/upload/v1234/test", result); - } - - @Test - public void testShorten() { - // should allow to shorted image/upload urls - String result = cloudinary.url().shorten(true).generate("test"); - assertEquals("http://res.cloudinary.com/test123/iu/test", result); - } - - @Test - public void testPrivateDownload() throws Exception { - String url = cloudinary.privateDownload("img", "jpg", Cloudinary.emptyMap()); - URI uri = new URI(url); - Map parameters = getUrlParameters(uri); - assertEquals("img", parameters.get("public_id")); - assertEquals("jpg", parameters.get("format")); - assertEquals("a", parameters.get("api_key")); - assertEquals("/v1_1/test123/image/download", uri.getPath()); - } - - @Test - public void testZipDownload() throws Exception { - String url = cloudinary.zipDownload("ttag", Cloudinary.emptyMap()); - URI uri = new URI(url); - Map parameters = getUrlParameters(uri); - assertEquals("ttag", parameters.get("tag")); - assertEquals("a", parameters.get("api_key")); - assertEquals("/v1_1/test123/image/download_tag.zip", uri.getPath()); - } - - @Test - public void testSpriteCss() { - String result = cloudinary.url().generateSpriteCss("test"); - assertEquals("http://res.cloudinary.com/test123/image/sprite/test.css", result); - result = cloudinary.url().generateSpriteCss("test.css"); - assertEquals("http://res.cloudinary.com/test123/image/sprite/test.css", result); - } - - @SuppressWarnings("unchecked") - @Test - public void testEscapePublicId() { - // should escape public_ids - Map tests = Cloudinary.asMap("a b", "a%20b", "a+b", "a%2Bb", "a%20b", "a%20b", "a-b", "a-b", "a??b", "a%3F%3Fb"); - for (Map.Entry entry : tests.entrySet()) { - String result = cloudinary.url().generate(entry.getKey()); - assertEquals("http://res.cloudinary.com/test123/image/upload/" + entry.getValue(), result); - } - } - - @Test - public void testSignedUrl() { - // should correctly sign a url - String expected = "http://res.cloudinary.com/test123/image/upload/s--MaRXzoEC--/c_crop,h_20,w_10/v1234/image.jpg"; - String actual = cloudinary.url().version(1234).transformation(new Transformation().crop("crop").width(10).height(20)).signed(true) - .generate("image.jpg"); - assertEquals(expected, actual); - - expected = "http://res.cloudinary.com/test123/image/upload/s--ZlgFLQcO--/v1234/image.jpg"; - actual = cloudinary.url().version(1234).signed(true).generate("image.jpg"); - assertEquals(expected, actual); - - expected = "http://res.cloudinary.com/test123/image/upload/s--Ai4Znfl3--/c_crop,h_20,w_10/image.jpg"; - actual = cloudinary.url().transformation(new Transformation().crop("crop").width(10).height(20)).signed(true).generate("image.jpg"); - assertEquals(expected, actual); - } - - @Test - public void testResponsiveWidth() { - // should support responsive width - Transformation trans = new Transformation().width(100).height(100).crop("crop").responsiveWidth(true); - String result = cloudinary.url().transformation(trans).generate("test"); - assertTrue(trans.isResponsive()); - assertEquals("http://res.cloudinary.com/test123/image/upload/c_crop,h_100,w_100/c_limit,w_auto/test", result); - Transformation.setResponsiveWidthTransformation(Cloudinary.asMap("width", "auto", "crop", "pad")); - trans = new Transformation().width(100).height(100).crop("crop").responsiveWidth(true); - result = cloudinary.url().transformation(trans).generate("test"); - assertTrue(trans.isResponsive()); - assertEquals("http://res.cloudinary.com/test123/image/upload/c_crop,h_100,w_100/c_pad,w_auto/test", result); - Transformation.setResponsiveWidthTransformation(null); - } - - public void testUtils() { - assertEquals(Cloudinary.asBoolean(true, null), true); - assertEquals(Cloudinary.asBoolean(false, null), false); - } - - public static Map getUrlParameters(URI uri) throws UnsupportedEncodingException { - Map params = new HashMap(); - for (String param : uri.getQuery().split("&")) { - String pair[] = param.split("="); - String key = URLDecoder.decode(pair[0], "UTF-8"); - String value = ""; - if (pair.length > 1) { - value = URLDecoder.decode(pair[1], "UTF-8"); - } - params.put(new String(key), new String(value)); - } - return params; - } + private > T randomEnum(Class clazz, Random random) { + return clazz.getEnumConstants()[random.nextInt(clazz.getEnumConstants().length)]; + } } diff --git a/cloudinary-core/src/test/java/com/cloudinary/test/UploaderTest.java b/cloudinary-core/src/test/java/com/cloudinary/test/UploaderTest.java deleted file mode 100644 index 17d92c8e..00000000 --- a/cloudinary-core/src/test/java/com/cloudinary/test/UploaderTest.java +++ /dev/null @@ -1,363 +0,0 @@ -package com.cloudinary.test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assume.assumeNotNull; - -import java.io.IOException; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import java.awt.Rectangle; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import com.cloudinary.Cloudinary; -import com.cloudinary.Coordinates; -import com.cloudinary.Transformation; - -@SuppressWarnings({"rawtypes", "unchecked"}) -public class UploaderTest { - - private Cloudinary cloudinary; - - @BeforeClass - public static void setUpClass() { - Cloudinary cloudinary = new Cloudinary(); - if (cloudinary.getStringConfig("api_secret") == null) { - System.err.println("Please setup environment for Upload test to run"); - } - } - - @Before - public void setUp() { - this.cloudinary = new Cloudinary(); - assumeNotNull(cloudinary.getStringConfig("api_secret")); - } - - @Test - public void testUpload() throws IOException { - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("colors", true)); - assertEquals(result.get("width"), 241L); - assertEquals(result.get("height"), 51L); - assertNotNull(result.get("colors")); - assertNotNull(result.get("predominant")); - Map to_sign = new HashMap(); - to_sign.put("public_id", (String) result.get("public_id")); - to_sign.put("version", Cloudinary.asString(result.get("version"))); - String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.getStringConfig("api_secret")); - assertEquals(result.get("signature"), expected_signature); - } - - @Test - public void testUploadUrl() throws IOException { - Map result = cloudinary.uploader().upload("http://cloudinary.com/images/logo.png", Cloudinary.emptyMap()); - assertEquals(result.get("width"), 241L); - assertEquals(result.get("height"), 51L); - Map to_sign = new HashMap(); - to_sign.put("public_id", (String) result.get("public_id")); - to_sign.put("version", Cloudinary.asString(result.get("version"))); - String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.getStringConfig("api_secret")); - assertEquals(result.get("signature"), expected_signature); - } - - @Test - public void testUploadDataUri() throws IOException { - Map result = cloudinary.uploader().upload("data:image/png;base64,iVBORw0KGgoAA\nAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0l\nEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6\nP9/AFGGFyjOXZtQAAAAAElFTkSuQmCC", Cloudinary.emptyMap()); - assertEquals(result.get("width"), 16L); - assertEquals(result.get("height"), 16L); - Map to_sign = new HashMap(); - to_sign.put("public_id", (String) result.get("public_id")); - to_sign.put("version", Cloudinary.asString(result.get("version"))); - String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.getStringConfig("api_secret")); - assertEquals(result.get("signature"), expected_signature); - } - - @Test - public void testRename() throws Exception { - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.emptyMap()); - - cloudinary.uploader().rename((String) result.get("public_id"), result.get("public_id")+"2", Cloudinary.emptyMap()); - assertNotNull(cloudinary.api().resource(result.get("public_id")+"2", Cloudinary.emptyMap())); - - Map result2 = cloudinary.uploader().upload("src/test/resources/favicon.ico", Cloudinary.emptyMap()); - boolean error_found=false; - try { - cloudinary.uploader().rename((String) result2.get("public_id"), result.get("public_id")+"2", Cloudinary.emptyMap()); - } catch(Exception e) { - error_found=true; - } - assertTrue(error_found); - cloudinary.uploader().rename((String) result2.get("public_id"), result.get("public_id")+"2", Cloudinary.asMap("overwrite", Boolean.TRUE)); - assertEquals(cloudinary.api().resource(result.get("public_id")+"2", Cloudinary.emptyMap()).get("format"), "ico"); - } - - @Test - public void testUniqueFilename() throws Exception { - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("use_filename", true)); - assertTrue(((String) result.get("public_id")).matches("logo_[a-z0-9]{6}")); - result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("use_filename", true, "unique_filename", false)); - assertEquals((String) result.get("public_id"), "logo"); - } - @Test - public void testExplicit() throws IOException { - Map result = cloudinary.uploader().explicit("cloudinary", Cloudinary.asMap("eager", Collections.singletonList(new Transformation().crop("scale").width(2.0)), "type", "twitter_name")); - String url = cloudinary.url().type("twitter_name").transformation(new Transformation().crop("scale").width(2.0)).format("png").version(result.get("version")).generate("cloudinary"); - String eagerUrl = (String) ((Map) ((List)result.get("eager")).get(0)).get("url"); - String cloudName = cloudinary.getStringConfig("cloud_name"); - assertEquals(eagerUrl.substring(eagerUrl.indexOf(cloudName)), url.substring(url.indexOf(cloudName))); - } - - @Test - public void testEager() throws IOException { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("eager", Collections.singletonList(new Transformation().crop("scale").width(2.0)))); - } - - @Test - public void testHeaders() throws IOException { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("headers", new String[]{"Link: 1"})); - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("headers", Cloudinary.asMap("Link", "1"))); - } - - @Test - public void testText() throws IOException { - Map result = cloudinary.uploader().text("hello world", Cloudinary.emptyMap()); - assertTrue(((Long) result.get("width")) > 1); - assertTrue(((Long) result.get("height")) > 1); - } - - @Test - public void testImageUploadTag() { - String tag = cloudinary.uploader().imageUploadTag("test-field", Cloudinary.asMap("callback", "http://localhost/cloudinary_cors.html"), Cloudinary.asMap("htmlattr", "htmlvalue")); - assertTrue(tag.contains("type='file'")); - assertTrue(tag.contains("data-cloudinary-field='test-field'")); - assertTrue(tag.contains("class='cloudinary-fileupload'")); - assertTrue(tag.contains("htmlattr='htmlvalue'")); - tag = cloudinary.uploader().imageUploadTag("test-field", Cloudinary.asMap("callback", "http://localhost/cloudinary_cors.html"), Cloudinary.asMap("class", "myclass")); - assertTrue(tag.contains("class='cloudinary-fileupload myclass'")); - } - - @Test - public void testSprite() throws IOException { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("tags", "sprite_test_tag", "public_id", "sprite_test_tag_1")); - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("tags", "sprite_test_tag", "public_id", "sprite_test_tag_2")); - Map result = cloudinary.uploader().generate_sprite("sprite_test_tag", Cloudinary.emptyMap()); - assertEquals(2, ((Map) result.get("image_infos")).size()); - result = cloudinary.uploader().generate_sprite("sprite_test_tag", Cloudinary.asMap("transformation", "w_100")); - assertTrue(((String) result.get("css_url")).contains("w_100")); - result = cloudinary.uploader().generate_sprite("sprite_test_tag", Cloudinary.asMap("transformation", new Transformation().width(100), "format", "jpg")); - assertTrue(((String) result.get("css_url")).contains("f_jpg,w_100")); - } - - @Test - public void testMulti() throws IOException { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("tags", "multi_test_tag", "public_id", "multi_test_tag_1")); - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("tags", "multi_test_tag", "public_id", "multi_test_tag_2")); - Map result = cloudinary.uploader().multi("multi_test_tag", Cloudinary.emptyMap()); - assertTrue(((String) result.get("url")).endsWith(".gif")); - result = cloudinary.uploader().multi("multi_test_tag", Cloudinary.asMap("transformation", "w_100")); - assertTrue(((String) result.get("url")).contains("w_100")); - result = cloudinary.uploader().multi("multi_test_tag", Cloudinary.asMap("transformation", new Transformation().width(111), "format", "pdf")); - assertTrue(((String) result.get("url")).contains("w_111")); - assertTrue(((String) result.get("url")).endsWith(".pdf")); - } - - @Test - public void testTags() throws Exception { - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.emptyMap()); - String public_id = (String)result.get("public_id"); - Map result2 = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.emptyMap()); - String public_id2 = (String)result2.get("public_id"); - cloudinary.uploader().addTag("tag1", new String[]{public_id, public_id2}, Cloudinary.emptyMap()); - cloudinary.uploader().addTag("tag2", new String[]{public_id}, Cloudinary.emptyMap()); - List tags = (List) cloudinary.api().resource(public_id, Cloudinary.emptyMap()).get("tags"); - assertEquals(tags, Cloudinary.asArray(new String[]{"tag1", "tag2"})); - tags = (List) cloudinary.api().resource(public_id2, Cloudinary.emptyMap()).get("tags"); - assertEquals(tags, Cloudinary.asArray(new String[]{"tag1"})); - cloudinary.uploader().removeTag("tag1", new String[]{public_id}, Cloudinary.emptyMap()); - tags = (List) cloudinary.api().resource(public_id, Cloudinary.emptyMap()).get("tags"); - assertEquals(tags, Cloudinary.asArray(new String[]{"tag2"})); - cloudinary.uploader().replaceTag("tag3", new String[]{public_id}, Cloudinary.emptyMap()); - tags = (List) cloudinary.api().resource(public_id, Cloudinary.emptyMap()).get("tags"); - assertEquals(tags, Cloudinary.asArray(new String[]{"tag3"})); - } - - @Test - public void testAllowedFormats() throws Exception { - //should allow whitelisted formats if allowed_formats - String[] formats = {"png"}; - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("allowed_formats", formats)); - assertEquals(result.get("format"), "png"); - } - - @Test - public void testAllowedFormatsWithIllegalFormat() throws Exception { - //should prevent non whitelisted formats from being uploaded if allowed_formats is specified - boolean errorFound = false; - String[] formats = {"jpg"}; - try{ - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("allowed_formats", formats)); - } catch(Exception e) { - errorFound=true; - } - assertTrue(errorFound); - } - - @Test - public void testAllowedFormatsWithFormat() throws Exception { - //should allow non whitelisted formats if type is specified and convert to that type - String[] formats = {"jpg"}; - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("allowed_formats", formats, "format", "jpg")); - assertEquals("jpg", result.get("format")); - } - - @Test - public void testFaceCoordinates() throws Exception { - //should allow sending face coordinates - Coordinates coordinates = new Coordinates(); - Rectangle rect1 = new Rectangle(121,31,110,151); - Rectangle rect2 = new Rectangle(120,30,109,150); - coordinates.addRect(rect1); - coordinates.addRect(rect2); - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("face_coordinates", coordinates, "faces", true)); - org.json.simple.JSONArray resultFaces = (org.json.simple.JSONArray) result.get("faces"); - assertEquals(2, resultFaces.size()); - - Object[] resultCoordinates = ((org.json.simple.JSONArray) resultFaces.get(0)).toArray(); - - assertEquals((long)rect1.x, resultCoordinates[0]); - assertEquals((long)rect1.y, resultCoordinates[1]); - assertEquals((long)rect1.width, resultCoordinates[2]); - assertEquals((long)rect1.height, resultCoordinates[3]); - - resultCoordinates = ((org.json.simple.JSONArray) resultFaces.get(1)).toArray(); - - assertEquals((long)rect2.x, resultCoordinates[0]); - assertEquals((long)rect2.y, resultCoordinates[1]); - assertEquals((long)rect2.width, resultCoordinates[2]); - assertEquals((long)rect2.height, resultCoordinates[3]); - - Coordinates differentCoordinates = new Coordinates(); - Rectangle rect3 = new Rectangle(122,32,111,152); - differentCoordinates.addRect(rect3); - cloudinary.uploader().explicit((String) result.get("public_id"), Cloudinary.asMap("face_coordinates", differentCoordinates, "faces", true, "type", "upload")); - Map info = cloudinary.api().resource((String) result.get("public_id"), Cloudinary.asMap("faces", true)); - - resultFaces = (org.json.simple.JSONArray) info.get("faces"); - assertEquals(1, resultFaces.size()); - resultCoordinates = ((org.json.simple.JSONArray) resultFaces.get(0)).toArray(); - - assertEquals((long)rect3.x, resultCoordinates[0]); - assertEquals((long)rect3.y, resultCoordinates[1]); - assertEquals((long)rect3.width, resultCoordinates[2]); - assertEquals((long)rect3.height, resultCoordinates[3]); - - } - - @Test - public void testCustomCoordinates() throws Exception { - //should allow sending face coordinates - Coordinates coordinates = new Coordinates("121,31,110,151"); - Map uploadResult = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("custom_coordinates", coordinates)); - Map result = cloudinary.api().resource(uploadResult.get("public_id").toString(), Cloudinary.asMap("coordinates", true)); - long[] expected = new long[]{121L,31L,110L,151L}; - Object[] actual = ((org.json.simple.JSONArray)((org.json.simple.JSONArray)((Map)result.get("coordinates")).get("custom")).get(0)).toArray(); - for (int i = 0; i < expected.length; i++){ - assertEquals(expected[i], actual[i]); - } - - coordinates = new Coordinates(new int[]{122,32,110,152}); - cloudinary.uploader().explicit((String) uploadResult.get("public_id"), Cloudinary.asMap("custom_coordinates", coordinates, "coordinates", true, "type", "upload")); - result = cloudinary.api().resource(uploadResult.get("public_id").toString(), Cloudinary.asMap("coordinates", true)); - expected = new long[]{122L,32L,110L,152L}; - actual = ((org.json.simple.JSONArray)((org.json.simple.JSONArray)((Map)result.get("coordinates")).get("custom")).get(0)).toArray(); - for (int i = 0; i < expected.length; i++){ - assertEquals(expected[i], actual[i]); - } - } - - @Test - public void testContext() throws Exception { - //should allow sending context - Map context = Cloudinary.asMap("caption", "some caption", "alt", "alternative"); - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("context", context)); - Map info = cloudinary.api().resource((String) result.get("public_id"), Cloudinary.asMap("context", true)); - assertEquals(Cloudinary.asMap("custom", context), info.get("context")); - Map differentContext = Cloudinary.asMap("caption", "different caption", "alt2", "alternative alternative"); - cloudinary.uploader().explicit((String) result.get("public_id"), Cloudinary.asMap("type", "upload", "context", differentContext)); - info = cloudinary.api().resource((String) result.get("public_id"), Cloudinary.asMap("context", true)); - assertEquals(Cloudinary.asMap("custom", differentContext), info.get("context")); - } - - @Test - public void testModerationRequest() throws Exception { - //should support requesting manual moderation - Map result = cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("moderation", "manual")); - assertEquals("manual", ((Map) ((List) result.get("moderation")).get(0)).get("kind")); - assertEquals("pending", ((Map) ((List) result.get("moderation")).get(0)).get("status")); - } - - - @Test - public void testRawConvertRequest() { - //should support requesting raw conversion - try { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("raw_convert", "illegal")); - } catch(Exception e) { - assertTrue(e.getMessage().matches("(.*)(Illegal value|not a valid)(.*)")); - } - } - - @Test - public void testCategorizationRequest() { - //should support requesting categorization - try { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("categorization", "illegal")); - } catch(Exception e) { - assertTrue(e.getMessage().matches("(.*)(Illegal value|not a valid)(.*)")); - } - } - - @Test - public void testDetectionRequest() { - //should support requesting detection - try { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("detection", "illegal")); - } catch(Exception e) { - assertTrue(e.getMessage().matches("(.*)(Illegal value|not a valid)(.*)")); - } - } - - @Test - public void testAutoTaggingRequest() { - //should support requesting auto tagging - try { - cloudinary.uploader().upload("src/test/resources/logo.png", Cloudinary.asMap("auto_tagging", 0.5f)); - } catch(Exception e) { - assertTrue(e.getMessage().matches("^Must use(.*)")); - } - } - - @Test - public void testUploadLargeRawFiles() throws Exception { - // support uploading large raw files - Map response = cloudinary.uploader().uploadLargeRaw("src/test/resources/docx.docx", Cloudinary.emptyMap()); - assertEquals(new java.io.File("src/test/resources/docx.docx").length(), response.get("bytes")); - assertEquals(Boolean.TRUE, response.get("done")); - } - - @Test - public void testUnsignedUpload() throws Exception { - // should support unsigned uploading using presets - Map preset = cloudinary.api().createUploadPreset(Cloudinary.asMap("folder", "upload_folder", "unsigned", true)); - Map result = cloudinary.uploader().unsignedUpload("src/test/resources/logo.png", preset.get("name").toString(), Cloudinary.emptyMap()); - assertTrue(result.get("public_id").toString().matches("^upload_folder\\/[a-z0-9]+$")); - cloudinary.api().deleteUploadPreset(preset.get("name").toString(), Cloudinary.emptyMap()); - } - -} diff --git a/cloudinary-core/src/test/java/com/cloudinary/transformation/ExpressionTest.java b/cloudinary-core/src/test/java/com/cloudinary/transformation/ExpressionTest.java new file mode 100644 index 00000000..fcc5db6f --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/transformation/ExpressionTest.java @@ -0,0 +1,189 @@ +package com.cloudinary.transformation; + +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +public class ExpressionTest { + + @Test + public void normalize_null_null() { + String result = Expression.normalize(null); + assertNull(result); + } + + @Test + public void normalize_number_number() { + String result = Expression.normalize(10); + assertEquals("10", result); + } + + @Test + public void normalize_emptyString_emptyString() { + String result = Expression.normalize(""); + assertEquals("", result); + } + + @Test + public void normalize_singleSpace_underscore() { + String result = Expression.normalize(" "); + assertEquals("_", result); + } + + @Test + public void normalize_blankString_underscore() { + String result = Expression.normalize(" "); + assertEquals("_", result); + } + + @Test + public void normalize_underscore_underscore() { + String result = Expression.normalize("_"); + assertEquals("_", result); + } + + @Test + public void normalize_underscores_underscore() { + String result = Expression.normalize("___"); + assertEquals("_", result); + } + + @Test + public void normalize_underscoresAndSpaces_underscore() { + String result = Expression.normalize(" _ __ _"); + assertEquals("_", result); + } + + @Test + public void normalize_arbitraryText_isNotAffected() { + String result = Expression.normalize("foobar"); + assertEquals("foobar", result); + } + + @Test + public void normalize_doubleAmpersand_replacedWithAndOperator() { + String result = Expression.normalize("foo && bar"); + assertEquals("foo_and_bar", result); + } + + @Test + public void normalize_doubleAmpersandWithNoSpaceAtEnd_isNotAffected() { + String result = Expression.normalize("foo&&bar"); + assertEquals("foo&&bar", result); + } + + @Test + public void normalize_width_recognizedAsVariableAndReplacedWithW() { + String result = Expression.normalize("width"); + assertEquals("w", result); + } + + @Test + public void normalize_initialAspectRatio_recognizedAsVariableAndReplacedWithIar() { + String result = Expression.normalize("initial_aspect_ratio"); + assertEquals("iar", result); + } + + @Test + public void normalize_dollarWidth_recognizedAsUserVariableAndNotAffected() { + String result = Expression.normalize("$width"); + assertEquals("$width", result); + } + + @Test + public void normalize_dollarInitialAspectRatio_recognizedAsUserVariableAndAsVariableReplacedWithAr() { + String result = Expression.normalize("$initial_aspect_ratio"); + assertEquals("$initial_ar", result); + } + + @Test + public void normalize_dollarMyWidth_recognizedAsUserVariableAndNotAffected() { + String result = Expression.normalize("$mywidth"); + assertEquals("$mywidth", result); + } + + @Test + public void normalize_dollarWidthWidth_recognizedAsUserVariableAndNotAffected() { + String result = Expression.normalize("$widthwidth"); + assertEquals("$widthwidth", result); + } + + @Test + public void normalize_dollarUnderscoreWidth_recognizedAsUserVariableAndNotAffected() { + String result = Expression.normalize("$_width"); + assertEquals("$_width", result); + } + + @Test + public void normalize_dollarUnderscoreX2Width_recognizedAsUserVariableAndNotAffected() { + String result = Expression.normalize("$__width"); + assertEquals("$_width", result); + } + + @Test + public void normalize_dollarX2Width_recognizedAsUserVariableAndNotAffected() { + String result = Expression.normalize("$$width"); + assertEquals("$$width", result); + } + + @Test + public void normalize_doesntReplaceVariable_1() { + String actual = Expression.normalize("$height_100"); + assertEquals("$height_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_2() { + String actual = Expression.normalize("$heightt_100"); + assertEquals("$heightt_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_3() { + String actual = Expression.normalize("$$height_100"); + assertEquals("$$height_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_4() { + String actual = Expression.normalize("$heightmy_100"); + assertEquals("$heightmy_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_5() { + String actual = Expression.normalize("$myheight_100"); + assertEquals("$myheight_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_6() { + String actual = Expression.normalize("$heightheight_100"); + assertEquals("$heightheight_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_7() { + String actual = Expression.normalize("$theheight_100"); + assertEquals("$theheight_100", actual); + } + + @Test + public void normalize_doesntReplaceVariable_8() { + String actual = Expression.normalize("$__height_100"); + assertEquals("$_height_100", actual); + } + + @Test + public void normalize_duration() { + String actual = Expression.normalize("duration"); + assertEquals("du", actual); + } + + @Test + public void normalize_previewDuration() { + String actual = Expression.normalize("preview:duration_2"); + assertEquals("preview:duration_2", actual); + } +} diff --git a/cloudinary-core/src/test/java/com/cloudinary/transformation/LayerTest.java b/cloudinary-core/src/test/java/com/cloudinary/transformation/LayerTest.java new file mode 100644 index 00000000..ca230f52 --- /dev/null +++ b/cloudinary-core/src/test/java/com/cloudinary/transformation/LayerTest.java @@ -0,0 +1,142 @@ +package com.cloudinary.transformation; + +import com.cloudinary.Cloudinary; +import com.cloudinary.Transformation; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import static org.junit.Assert.*; + +/** + * Created by amir on 03/11/2015. + */ +public class LayerTest { + private static final String DEFAULT_ROOT_PATH = "https://res.cloudinary.com/test123/"; + private static final String DEFAULT_UPLOAD_PATH = DEFAULT_ROOT_PATH + "image/upload/"; + private static final String VIDEO_UPLOAD_PATH = DEFAULT_ROOT_PATH + "video/upload/"; + private Cloudinary cloudinary; + + @Before + public void setUp() { + this.cloudinary = new Cloudinary("cloudinary://a:b@test123?load_strategies=false&analytics=false"); + } + + @After + public void tearDown() throws Exception { + + } + + @Test + public void testOverlay() { + // should support overlay + Transformation transformation = new Transformation().overlay("text:hello"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "l_text:hello/test", result); + // should not pass width/height to html if overlay + transformation = new Transformation().overlay("text:hello").width(100).height(100); + result = cloudinary.url().transformation(transformation).generate("test"); + assertNull(transformation.getHtmlHeight()); + assertNull(transformation.getHtmlWidth()); + assertEquals(DEFAULT_UPLOAD_PATH + "h_100,l_text:hello,w_100/test", result); + + transformation = new Transformation().overlay(new TextLayer().text("goodbye")); + result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "l_text:goodbye/test", result); + } + + @Test + public void testUnderlay() { + Transformation transformation = new Transformation().underlay("text:hello"); + String result = cloudinary.url().transformation(transformation).generate("test"); + assertEquals(DEFAULT_UPLOAD_PATH + "u_text:hello/test", result); + // should not pass width/height to html if underlay + transformation = new Transformation().underlay("text:hello").width(100).height(100); + result = cloudinary.url().transformation(transformation).generate("test"); + assertNull(transformation.getHtmlHeight()); + assertNull(transformation.getHtmlWidth()); + assertEquals(DEFAULT_UPLOAD_PATH + "h_100,u_text:hello,w_100/test", result); + } + + @Test + public void testPublicIdWithDoubleUnderscoresInOverlay() { + Transformation transformation = new Transformation().width(300).height(200).crop("fill").overlay("my__lake"); + String result = cloudinary.url().transformation(transformation).generate("sample.jpg"); + assertEquals(DEFAULT_UPLOAD_PATH + "c_fill,h_200,l_my__lake,w_300/sample.jpg", result); + } + + @Test + public void testLayerOptions() { + Object tests[] = { + new Layer().publicId("logo"), + "logo", + new Layer().publicId("logo__111"), + "logo__111", + new Layer().publicId("folder/logo"), + "folder:logo", + new Layer().publicId("logo").type("private"), + "private:logo", + new Layer().publicId("logo").format("png"), + "logo.png", + new Layer().resourceType("video").publicId("cat"), + "video:cat", + new TextLayer().text("Hello/World").fontFamily("Arial").fontSize(18), + "text:Arial_18:Hello%252FWorld", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18), + "text:Arial_18:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new TextLayer().text("Hello World, Nice to meet you?").fontFamily("Arial").fontSize(18) + .fontWeight("bold").fontStyle("italic").letterSpacing("4"), + "text:Arial_18_bold_italic_letter_spacing_4:Hello%20World%252C%20Nice%20to%20meet%20you%3F", + new SubtitlesLayer().publicId("sample_sub_en.srt"), "subtitles:sample_sub_en.srt", + new SubtitlesLayer().publicId("sample_sub_he.srt").fontFamily("Arial").fontSize(40), + "subtitles:Arial_40:sample_sub_he.srt"}; + + for (int i = 0; i < tests.length; i += 2) { + Object layer = tests[i]; + String expected = (String) tests[i + 1]; + assertEquals(expected, layer.toString()); + } + } + + @Test(expected = IllegalArgumentException.class) + public void testOverlayError1() { + // Must supply font_family for text in overlay + cloudinary.url().transformation(new Transformation().overlay(new TextLayer().fontStyle("italic"))).generate("test"); + } + + @Test(expected = IllegalArgumentException.class) + public void testOverlayError2() { + // Must supply public_id for for non-text underlay + cloudinary.url().transformation(new Transformation().underlay(new Layer().resourceType("video"))).generate("test"); + } + + @Test + public void testResourceType() throws Exception { + + } + + @Test + public void testType() throws Exception { + + } + + @Test + public void testPublicId() throws Exception { + + } + + @Test + public void testFormat() throws Exception { + + } + + @Test + public void testToString() throws Exception { + + } + + @Test + public void testFormattedPublicId() throws Exception { + + } +} diff --git a/cloudinary-http5/build.gradle b/cloudinary-http5/build.gradle new file mode 100644 index 00000000..07f6c8a6 --- /dev/null +++ b/cloudinary-http5/build.gradle @@ -0,0 +1,28 @@ +plugins { + id 'java-library' +} + +apply from: "../java_shared.gradle" +apply from: "../publish.gradle" + +task ciTest( type: Test ) { + useJUnit { + excludeCategories 'com.cloudinary.test.TimeoutTest' + if (System.getProperty("CLOUDINARY_ACCOUNT_URL") == "") { + exclude '**/AccountApiTest.class' + } + } +} + +dependencies { + compile project(':cloudinary-core') + compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.18.0' + api group: 'org.apache.httpcomponents.client5', name: 'httpclient5', version: '5.3.1' + api group: 'org.apache.httpcomponents.core5', name: 'httpcore5', version: '5.2.5' + testCompile project(':cloudinary-test-common') + testCompile group: 'org.hamcrest', name: 'java-hamcrest', version: '2.0.0.0' + testCompile group: 'pl.pragmatists', name: 'JUnitParams', version: '1.0.5' + testCompile group: 'junit', name: 'junit', version: '4.12' +} + +// Publishing configuration moved to ../publish.gradle diff --git a/cloudinary-http5/src/main/java/com/cloudinary/http5/ApiStrategy.java b/cloudinary-http5/src/main/java/com/cloudinary/http5/ApiStrategy.java new file mode 100644 index 00000000..9c0145e9 --- /dev/null +++ b/cloudinary-http5/src/main/java/com/cloudinary/http5/ApiStrategy.java @@ -0,0 +1,193 @@ +package com.cloudinary.http5; + + +import com.cloudinary.Api; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.exceptions.GeneralError; +import com.cloudinary.http5.api.Response; +import com.cloudinary.strategies.AbstractApiStrategy; +import com.cloudinary.utils.ObjectUtils; +import org.apache.hc.client5.http.classic.methods.*; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.UrlEncodedFormEntity; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.net.URIBuilder; +import org.apache.hc.core5.util.Timeout; +import org.cloudinary.json.JSONException; +import org.cloudinary.json.JSONObject; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +import static com.cloudinary.http5.ApiUtils.prepareParams; +import static com.cloudinary.http5.ApiUtils.setTimeouts; + +public class ApiStrategy extends AbstractApiStrategy { + + private static final String APACHE_HTTP_CLIENT_VERSION = System.getProperty("apache.http.client.version", "5.3.1"); + + private CloseableHttpClient client; + + public void init(Api api) { + super.init(api); + + HttpClientBuilder clientBuilder = HttpClients.custom(); + clientBuilder.useSystemProperties().setUserAgent(this.api.cloudinary.getUserAgent() + " ApacheHttpClient/" + APACHE_HTTP_CLIENT_VERSION); + + HttpClientConnectionManager connectionManager = (HttpClientConnectionManager) api.cloudinary.config.properties.get("connectionManager"); + if (connectionManager != null) { + clientBuilder.setConnectionManager(connectionManager); + } + + RequestConfig requestConfig = buildRequestConfig(); + + client = clientBuilder + .setDefaultRequestConfig(requestConfig) + .build(); + } + + public RequestConfig buildRequestConfig() { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + + if (api.cloudinary.config.proxyHost != null && api.cloudinary.config.proxyPort != 0) { + HttpHost proxy = new HttpHost(api.cloudinary.config.proxyHost, api.cloudinary.config.proxyPort); + requestConfigBuilder.setProxy(proxy); + } + + int timeout = this.api.cloudinary.config.timeout; + if (timeout > 0) { + requestConfigBuilder.setResponseTimeout(Timeout.ofSeconds(timeout)) + .setConnectionRequestTimeout(Timeout.ofSeconds(timeout)) + .setConnectTimeout(Timeout.ofSeconds(timeout)); + } + + return requestConfigBuilder.build(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + public ApiResponse callApi(Api.HttpMethod method, String apiUrl, Map params, Map options, String autorizationHeader) throws Exception { + HttpUriRequestBase request = prepareRequest(method, apiUrl, params, options); + + request.setHeader("Authorization", autorizationHeader); + + return getApiResponse(request); + } + + private ApiResponse getApiResponse(HttpUriRequestBase request) throws Exception { + String responseData = null; + int code = 0; + CloseableHttpResponse response; + try { + response = client.execute(request); + code = response.getCode(); + HttpEntity entity = response.getEntity(); + if (entity != null) { + responseData = EntityUtils.toString(entity, StandardCharsets.UTF_8); + } + } catch (IOException e) { + throw new GeneralError("Error executing request: " + e.getMessage()); + } + + if (code != 200) { + Map result; + try { + JSONObject responseJSON = new JSONObject(responseData); + result = ObjectUtils.toMap(responseJSON); + } catch (JSONException e) { + throw new RuntimeException("Invalid JSON response from server " + e.getMessage()); + } + + // Extract the error message from the result map + String message = (String) ((Map) result.get("error")).get("message"); + + // Get the appropriate exception class based on status code + Class exceptionClass = Api.CLOUDINARY_API_ERROR_CLASSES.get(code); + if (exceptionClass != null) { + Constructor exceptionConstructor = exceptionClass.getConstructor(String.class); + throw exceptionConstructor.newInstance(message); + } else { + throw new GeneralError("Server returned unexpected status code - " + code + " - " + responseData); + } + } + + Map result; + try { + JSONObject responseJSON = new JSONObject(responseData); + result = ObjectUtils.toMap(responseJSON); + } catch (JSONException e) { + throw new RuntimeException("Invalid JSON response from server " + e.getMessage()); + } + + return new Response(response, result); + } + + @Override + public ApiResponse callAccountApi(Api.HttpMethod method, String apiUrl, Map params, Map options, String authorizationHeader) throws Exception { + // Prepare the request + HttpUriRequestBase request = prepareRequest(method, apiUrl, params, options); + + // Add authorization header + + request.setHeader("Authorization", authorizationHeader); + + // Execute the request and return the response + return getApiResponse(request); + } + + private HttpUriRequestBase prepareRequest(Api.HttpMethod method, String apiUrl, Map params, Map options) throws URISyntaxException { + HttpUriRequestBase request; + + String contentType = ObjectUtils.asString(options.get("content_type"), "urlencoded"); + + switch (method) { + case GET: + URIBuilder uriBuilder = new URIBuilder(apiUrl); + for (NameValuePair param : prepareParams(params)) { + uriBuilder.addParameter(param.getName(), param.getValue()); + } + request = new HttpGet(uriBuilder.toString()); + break; + case POST: + request = new HttpPost(apiUrl); + setEntity((HttpUriRequestBase) request, params, contentType); + break; + case PUT: + request = new HttpPut(apiUrl); + setEntity((HttpUriRequestBase) request, params, contentType); + break; + case DELETE: + request = new HttpDelete(apiUrl); + setEntity((HttpUriRequestBase) request, params, contentType); + break; + default: + throw new IllegalArgumentException("Unknown HTTP method"); + } + setTimeouts(request, options); + return request; + } + + private void setEntity(HttpUriRequestBase request, Map params, String contentType) { + if ("json".equals(contentType)) { + JSONObject json = ObjectUtils.toJSON(params); + StringEntity entity = new StringEntity(json.toString(), StandardCharsets.UTF_8); + request.setEntity(entity); + request.setHeader("Content-Type", "application/json"); + } else { + List formParams = prepareParams(params); + request.setEntity(new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8)); + } + } +} diff --git a/cloudinary-http5/src/main/java/com/cloudinary/http5/ApiUtils.java b/cloudinary-http5/src/main/java/com/cloudinary/http5/ApiUtils.java new file mode 100644 index 00000000..040fd714 --- /dev/null +++ b/cloudinary-http5/src/main/java/com/cloudinary/http5/ApiUtils.java @@ -0,0 +1,72 @@ +package com.cloudinary.http5; + +import com.cloudinary.utils.ObjectUtils; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.NameValuePair; +import org.apache.hc.core5.http.message.BasicNameValuePair; +import org.apache.hc.core5.util.Timeout; +import org.cloudinary.json.JSONObject; + +import java.util.*; + +public final class ApiUtils { + private ApiUtils() {} + + public static void setTimeouts(HttpUriRequestBase request, Map options) { + RequestConfig config = request.getConfig(); + final RequestConfig.Builder builder; + + if (config != null) { + builder = RequestConfig.copy(config); + } else { + builder = RequestConfig.custom(); + } + + Integer timeout = (Integer) options.get("timeout"); + if (timeout != null) { + builder.setResponseTimeout(Timeout.ofSeconds(timeout)); + } + + Integer connectionRequestTimeout = (Integer) options.get("connection_request_timeout"); + if (connectionRequestTimeout != null) { + builder.setConnectionRequestTimeout(Timeout.ofSeconds(connectionRequestTimeout)); + } + + Integer connectTimeout = (Integer) options.get("connect_timeout"); + if (connectTimeout != null) { + builder.setConnectTimeout(Timeout.ofSeconds(connectTimeout)); + } + + request.setConfig(builder.build()); + } + + + public static List prepareParams(Map params) { + List requestParams = new ArrayList<>(); + + for (Map.Entry param : params.entrySet()) { + String key = param.getKey(); + Object value = param.getValue(); + + if (value instanceof Iterable) { + // If the value is an Iterable, handle each item individually + for (Object single : (Iterable) value) { + requestParams.add(new BasicNameValuePair(key + "[]", ObjectUtils.asString(single))); + } + } else if (value instanceof Map) { + // Convert Map to JSON string manually to avoid empty object issues + JSONObject jsonObject = new JSONObject(); + for (Map.Entry entry : ((Map) value).entrySet()) { + jsonObject.put(entry.getKey().toString(), entry.getValue()); + } + requestParams.add(new BasicNameValuePair(key, jsonObject.toString())); + } else { + // Handle simple key-value pairs + requestParams.add(new BasicNameValuePair(key, ObjectUtils.asString(value))); + } + } + + return requestParams; + } +} diff --git a/cloudinary-http5/src/main/java/com/cloudinary/http5/UploaderStrategy.java b/cloudinary-http5/src/main/java/com/cloudinary/http5/UploaderStrategy.java new file mode 100644 index 00000000..589dff5b --- /dev/null +++ b/cloudinary-http5/src/main/java/com/cloudinary/http5/UploaderStrategy.java @@ -0,0 +1,188 @@ +package com.cloudinary.http5; + +import com.cloudinary.ProgressCallback; +import com.cloudinary.Uploader; +import com.cloudinary.Util; +import com.cloudinary.strategies.AbstractUploaderStrategy; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; +import org.apache.hc.client5.http.classic.methods.HttpPost; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.client5.http.entity.mime.ByteArrayBody; +import org.apache.hc.client5.http.entity.mime.FileBody; +import org.apache.hc.client5.http.entity.mime.HttpMultipartMode; +import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.classic.HttpClients; +import org.apache.hc.client5.http.io.HttpClientConnectionManager; +import org.apache.hc.core5.http.ContentType; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.http.ParseException; +import org.apache.hc.core5.http.io.entity.EntityUtils; +import org.apache.hc.core5.util.Timeout; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.Map; + +public class UploaderStrategy extends AbstractUploaderStrategy { + + private static final String APACHE_HTTP_CLIENT_VERSION = System.getProperty("apache.http.client.version", "5.3.1"); + + private CloseableHttpClient client; + + @Override + public void init(Uploader uploader) { + super.init(uploader); + + HttpClientBuilder clientBuilder = HttpClients.custom(); + clientBuilder.useSystemProperties().setUserAgent(cloudinary().getUserAgent() + " ApacheHttpClient/" + APACHE_HTTP_CLIENT_VERSION); + + HttpClientConnectionManager connectionManager = (HttpClientConnectionManager) cloudinary().config.properties.get("connectionManager"); + if (connectionManager != null) { + clientBuilder.setConnectionManager(connectionManager); + } + + RequestConfig requestConfig = buildRequestConfig(); + + client = clientBuilder + .setDefaultRequestConfig(requestConfig) + .build(); + } + + public RequestConfig buildRequestConfig() { + RequestConfig.Builder requestConfigBuilder = RequestConfig.custom(); + + if (cloudinary().config.proxyHost != null && cloudinary().config.proxyPort != 0) { + HttpHost proxy = new HttpHost(cloudinary().config.proxyHost, cloudinary().config.proxyPort); + requestConfigBuilder.setProxy(proxy); + } + + int timeout = cloudinary().config.timeout; + if (timeout > 0) { + requestConfigBuilder.setResponseTimeout(Timeout.ofSeconds(timeout)) + .setConnectionRequestTimeout(Timeout.ofSeconds(timeout)) + .setConnectTimeout(Timeout.ofSeconds(timeout)); + } + + return requestConfigBuilder.build(); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + @Override + public Map callApi(String action, Map params, Map options, Object file, ProgressCallback progressCallback) throws IOException { + if (progressCallback != null) { + throw new IllegalArgumentException("Progress callback is not supported"); + } + + // Initialize options if passed as null + if (options == null) { + options = ObjectUtils.emptyMap(); + } + + boolean returnError = ObjectUtils.asBoolean(options.get("return_error"), false); + + if (requiresSigning(action, options)) { + uploader.signRequestParams(params, options); + } else { + Util.clearEmpty(params); + } + + String apiUrl = buildUploadUrl(action, options); + + // Prepare the request + HttpUriRequestBase request = prepareRequest(apiUrl, params, options, file); + + // Execute the request and handle the response + String responseData; + int code; + + try (CloseableHttpResponse response = client.execute(request)) { + code = response.getCode(); + responseData = EntityUtils.toString(response.getEntity()); + } catch (ParseException e) { + throw new RuntimeException(e); + } + + // Process and return the response + return processResponse(returnError, code, responseData); + } + + private HttpUriRequestBase prepareRequest(String apiUrl, Map params, Map options, Object file) throws IOException { + HttpPost request = new HttpPost(apiUrl); + + MultipartEntityBuilder multipartBuilder = MultipartEntityBuilder.create() + .setCharset(StandardCharsets.UTF_8).setMode(HttpMultipartMode.LEGACY); + + // Add text parameters + for (Map.Entry param : params.entrySet()) { + if (param.getValue() instanceof Collection) { + for (Object value : (Collection) param.getValue()) { + multipartBuilder.addTextBody(param.getKey() + "[]", ObjectUtils.asString(value), ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + } else { + String value = param.getValue().toString(); + if (StringUtils.isNotBlank(value)) { + multipartBuilder.addTextBody(param.getKey(), value, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + } + } + + // Add file part + addFilePart(multipartBuilder, file, options); + + request.setEntity(multipartBuilder.build()); + + // Add extra headers if provided + Map extraHeaders = (Map) options.get("extra_headers"); + if (extraHeaders != null) { + for (Map.Entry header : extraHeaders.entrySet()) { + request.addHeader(header.getKey(), header.getValue()); + } + } + + return request; + } + + + private void addFilePart(MultipartEntityBuilder multipartBuilder, Object file, Map options) throws IOException { + String filename = (String) options.get("filename"); + + if (file instanceof String && !StringUtils.isRemoteUrl((String) file)) { + File _file = new File((String) file); + if (!_file.isFile() || !_file.canRead()) { + throw new IOException("File not found or unreadable: " + file); + } + file = _file; + } + + if (file instanceof File) { + if (filename == null) { + filename = ((File) file).getName(); + } + // Encode filename properly + filename = new String(filename.getBytes(StandardCharsets.UTF_8), StandardCharsets.UTF_8); + + // Create FileBody with correct filename encoding + FileBody fileBody = new FileBody((File) file, ContentType.APPLICATION_OCTET_STREAM, filename); + multipartBuilder.addPart("file", fileBody); + } else if (file instanceof String) { + multipartBuilder.addTextBody("file", (String) file, ContentType.TEXT_PLAIN); + } else if (file instanceof byte[]) { + if (filename == null) { + filename = "file"; + } + ByteArrayBody byteArrayBody = new ByteArrayBody((byte[]) file, ContentType.APPLICATION_OCTET_STREAM, filename); + multipartBuilder.addPart("file", byteArrayBody); + } else if (file == null) { + // No file to add + } else { + throw new IOException("Unrecognized file parameter " + file); + } + } +} diff --git a/cloudinary-http5/src/main/java/com/cloudinary/http5/api/Response.java b/cloudinary-http5/src/main/java/com/cloudinary/http5/api/Response.java new file mode 100644 index 00000000..fd7b0980 --- /dev/null +++ b/cloudinary-http5/src/main/java/com/cloudinary/http5/api/Response.java @@ -0,0 +1,63 @@ +package com.cloudinary.http5.api; + +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.RateLimit; +import org.apache.hc.core5.http.Header; +import org.apache.hc.core5.http.HttpResponse; +import java.text.ParseException; + +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class Response extends HashMap implements ApiResponse { + private static final long serialVersionUID = -5458609797599845837L; + private final HttpResponse response; + + @SuppressWarnings("unchecked") + public Response(HttpResponse response, Map result) { + super(result); + this.response = response; + } + + public HttpResponse getRawHttpResponse() { + return this.response; + } + + private static final Pattern RATE_LIMIT_REGEX = Pattern + .compile("X-FEATURE(\\w*)RATELIMIT(-LIMIT|-RESET|-REMAINING)", Pattern.CASE_INSENSITIVE); + private static final String RFC1123_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + private static final DateFormat RFC1123 = new SimpleDateFormat(RFC1123_PATTERN, Locale.ENGLISH); + + public Map rateLimits() throws ParseException { + Header[] headers = this.response.getHeaders(); + Map limits = new HashMap<>(); + for (Header header : headers) { + Matcher m = RATE_LIMIT_REGEX.matcher(header.getName()); + if (m.matches()) { + String limitName = "Api"; + RateLimit limit = limits.getOrDefault(limitName, new RateLimit()); + if (!m.group(1).isEmpty()) { + limitName = m.group(1); + } + if (m.group(2).equalsIgnoreCase("-limit")) { + limit.setLimit(Long.parseLong(header.getValue())); + } else if (m.group(2).equalsIgnoreCase("-remaining")) { + limit.setRemaining(Long.parseLong(header.getValue())); + } else if (m.group(2).equalsIgnoreCase("-reset")) { + limit.setReset(RFC1123.parse(header.getValue())); + } + limits.put(limitName, limit); + } + } + return limits; + } + + public RateLimit apiRateLimit() throws ParseException { + return rateLimits().get("Api"); + } +} diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/AccountApiTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/AccountApiTest.java new file mode 100644 index 00000000..573a12e5 --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/AccountApiTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class AccountApiTest extends AbstractAccountApiTest { +} diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/ApiTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/ApiTest.java new file mode 100644 index 00000000..53da8866 --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/ApiTest.java @@ -0,0 +1,102 @@ +package com.cloudinary.test; + +import com.cloudinary.Cloudinary; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.http5.ApiStrategy; +import com.cloudinary.utils.ObjectUtils; +import org.apache.hc.client5.http.config.RequestConfig; +import org.apache.hc.core5.http.HttpHost; +import org.apache.hc.core5.util.Timeout; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import java.util.Map; +import java.util.UUID; + +import static com.cloudinary.utils.ObjectUtils.asMap; + + +public class ApiTest extends AbstractApiTest { + + @Test + public void testBuildRequestConfig_withProxyAndTimeout() { + Cloudinary cloudinary = new Cloudinary("cloudinary://test:test@test.com"); + cloudinary.config.proxyHost = "127.0.0.1"; + cloudinary.config.proxyPort = 8080; + cloudinary.config.timeout = 15; + + RequestConfig requestConfig = ((ApiStrategy)cloudinary.api().getStrategy()).buildRequestConfig(); + + assert(requestConfig.getProxy() != null); + HttpHost proxy = requestConfig.getProxy(); + assert("127.0.0.1" == proxy.getHostName()); + assert(8080 == proxy.getPort()); + + assert(15000 == requestConfig.getConnectionRequestTimeout().toMilliseconds()); + assert(15000 == requestConfig.getResponseTimeout().toMilliseconds()); + } + + @Test + public void testBuildRequestConfig_withoutProxy() { + Cloudinary cloudinary = new Cloudinary("cloudinary://test:test@test.com"); + cloudinary.config.timeout = 10; + + RequestConfig requestConfig = ((ApiStrategy)cloudinary.api().getStrategy()).buildRequestConfig(); + + assert(requestConfig.getProxy() == null); + assert(10000 == requestConfig.getConnectionRequestTimeout().toMilliseconds()); + assert(10000 == requestConfig.getResponseTimeout().toMilliseconds()); + } + + @Category(TimeoutTest.class) + @Test(expected = Exception.class) + public void testConnectTimeoutParameter() throws Exception { + Map options = asMap( + "max_results", 500, + "connect_timeout", 0.2); + + try { + System.out.println("Setting connect timeout to 100 ms"); + ApiResponse result = cloudinary.api().resources(options); + System.out.println("Request completed without timeout"); + } catch (Exception e) { + throw new Exception("Connection timeout", e); + } + } + + @Category(TimeoutTest.class) + @Test(expected = Exception.class) + public void testTimeoutParameter() throws Exception { + // Set a very short request timeout to trigger a timeout exception + Map options = asMap( + "max_results", 500, + "timeout", Timeout.ofMilliseconds(1000)); // Set the timeout to 1 second + + try { + ApiResponse result = cloudinary.api().resources(options); + } catch (Exception e) { + // Convert IOException to SocketTimeoutException if appropriate + throw new Exception("Socket timeout"); + } + } + + @Category(TimeoutTest.class) + @Test(expected = Exception.class) + public void testUploaderTimeoutParameter() throws Exception { + Cloudinary cloudinary = new Cloudinary("cloudinary://test:test@test.com"); + cloudinary.config.uploadPrefix = "https://10.255.255.1"; + String publicId = UUID.randomUUID().toString(); + // Set a very short request timeout to trigger a timeout exception + Map options = asMap( + "max_results", 500, + "timeout", Timeout.ofMilliseconds(10)); // Set the timeout to 1 second + + try { + Map result = cloudinary.uploader().addContext(asMap("caption", "new caption"), new String[]{publicId, "no-such-id"}, options); + } catch (Exception e) { + // Convert IOException to SocketTimeoutException if appropriate + throw new Exception("Socket timeout"); + } + } + +} \ No newline at end of file diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/ContextTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/ContextTest.java new file mode 100644 index 00000000..4841e9f6 --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/ContextTest.java @@ -0,0 +1,5 @@ +package com.cloudinary.test; + +public class ContextTest extends AbstractContextTest { + +} \ No newline at end of file diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/FoldersApiTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/FoldersApiTest.java new file mode 100644 index 00000000..971bcf39 --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/FoldersApiTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class FoldersApiTest extends AbstractFoldersApiTest { +} diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/SearchTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/SearchTest.java new file mode 100644 index 00000000..16a4708c --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/SearchTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class SearchTest extends AbstractSearchTest { +} diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/StreamingProfilesApiTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/StreamingProfilesApiTest.java new file mode 100644 index 00000000..4e763579 --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/StreamingProfilesApiTest.java @@ -0,0 +1,7 @@ +package com.cloudinary.test; + +/** + * Created by amir on 25/10/2016. + */ +public class StreamingProfilesApiTest extends AbstractStreamingProfilesApiTest { +} diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/StructuredMetadataTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/StructuredMetadataTest.java new file mode 100644 index 00000000..900da239 --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/StructuredMetadataTest.java @@ -0,0 +1,4 @@ +package com.cloudinary.test; + +public class StructuredMetadataTest extends AbstractStructuredMetadataTest { +} \ No newline at end of file diff --git a/cloudinary-http5/src/test/java/com/cloudinary/test/UploaderTest.java b/cloudinary-http5/src/test/java/com/cloudinary/test/UploaderTest.java new file mode 100644 index 00000000..50c2a6ed --- /dev/null +++ b/cloudinary-http5/src/test/java/com/cloudinary/test/UploaderTest.java @@ -0,0 +1,5 @@ +package com.cloudinary.test; + +public class UploaderTest extends AbstractUploaderTest { + +} \ No newline at end of file diff --git a/cloudinary-taglib/build.gradle b/cloudinary-taglib/build.gradle new file mode 100644 index 00000000..ef8824af --- /dev/null +++ b/cloudinary-taglib/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'java-library' +} + +apply from: "../java_shared.gradle" +apply from: "../publish.gradle" + +task ciTest( type: Test ) + +dependencies { + compile project(':cloudinary-core') + compile group: 'org.apache.commons', name: 'commons-lang3', version:'3.18.0' + testCompile group: 'org.hamcrest', name: 'java-hamcrest', version:'2.0.0.0' + testCompile group: 'pl.pragmatists', name: 'JUnitParams', version:'1.0.5' + testCompile group: 'junit', name: 'junit', version:'4.12' + compile(group: 'javax.servlet', name: 'jsp-api', version:'2.0') { + /* This dependency was originally in the Maven provided scope, but the project was not of type war. + This behavior is not yet supported by Gradle, so this dependency has been converted to a compile dependency. + Please review and delete this closure when resolved. */ + } +} + +// Publishing configuration moved to ../publish.gradle \ No newline at end of file diff --git a/cloudinary-taglib/pom.xml b/cloudinary-taglib/pom.xml deleted file mode 100644 index eb5f52db..00000000 --- a/cloudinary-taglib/pom.xml +++ /dev/null @@ -1,34 +0,0 @@ - - 4.0.0 - - - com.cloudinary - cloudinary-parent - 1.0.15-SNAPSHOT - - - cloudinary-taglib - jar - - Cloudinary Taglib Library - - - - com.cloudinary - cloudinary - ${project.version} - - - javax.servlet - jsp-api - 2.0 - provided - - - org.apache.commons - commons-lang3 - 3.1 - - - - diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/Singleton.java b/cloudinary-taglib/src/main/java/com/cloudinary/Singleton.java index 43453b9b..5c11458f 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/Singleton.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/Singleton.java @@ -1,23 +1,24 @@ package com.cloudinary; -/** This class contains a singleton in a generic way. This class is used by the tags to +/** + * This class contains a singleton in a generic way. This class is used by the tags to * retrieve the Cloudinary configuration. - * + *

* the containing framework is responsible for registering the cloudinary configuration with the * Singleton, and then removing it on shutdown. This allows the user to use Spring or any other * framework without imposing additional dependencies on the cloudinary project. - * - * @author jpollak * + * @author jpollak */ -public class Singleton { +public final class Singleton { + private Singleton() {} private static Cloudinary cloudinary; - + public static void registerCloudinary(Cloudinary cloudinary) { Singleton.cloudinary = cloudinary; } - + public static void deregisterCloudinary() { cloudinary = null; } @@ -25,7 +26,7 @@ public static void deregisterCloudinary() { private static class DefaultCloudinaryHolder { public static final Cloudinary INSTANCE = new Cloudinary(); } - + public static Cloudinary getCloudinary() { if (cloudinary == null) { return DefaultCloudinaryHolder.INSTANCE; diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/SingletonManager.java b/cloudinary-taglib/src/main/java/com/cloudinary/SingletonManager.java index 0b1c1248..5983bb84 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/SingletonManager.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/SingletonManager.java @@ -3,15 +3,15 @@ public class SingletonManager { private Cloudinary cloudinary; - + public void setCloudinary(Cloudinary cloudinary) { this.cloudinary = cloudinary; } - + public void init() { Singleton.registerCloudinary(cloudinary); } - + public void destroy() { Singleton.deregisterCloudinary(); } diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryImageTag.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryImageTag.java index ec36673d..8b253533 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryImageTag.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryImageTag.java @@ -4,91 +4,53 @@ import java.util.HashMap; import java.util.Map; -import javax.servlet.ServletRequest; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; -import javax.servlet.jsp.PageContext; -import javax.servlet.jsp.tagext.DynamicAttributes; -import javax.servlet.jsp.tagext.SimpleTagSupport; import com.cloudinary.*; /** - * - * - * Transformation transformation = new Transformation().width(100).height(101).crop("crop"); - * String result = cloudinary.url().transformation(transformation).imageTag("test", - * Cloudinary.asMap("alt", "my image")); - * - * my image - * + * Generates an image html tag.
+ * For example,
+ * {@code } + *
is equivalent to:
+ *

{@code
+ * Transformation transformation = new Transformation()
+ *      .width(100)
+ *      .height(101)
+ *      .crop("crop");
+ * String result = cloudinary.url()
+ *      .transformation(transformation)
+ *      .imageTag("test", Cloudinary.asMap("alt", "my image"));
+ * }
+ *
+ * Both code segments above produce the following tag:
+ * {@code my image } + *
* @author jpollak * */ -public class CloudinaryImageTag extends SimpleTagSupport implements DynamicAttributes { +public class CloudinaryImageTag extends CloudinaryUrl { private String id = null; private String extraClasses = null; - - private String src = null; - private StoredFile storedSrc = null; - - private String type = null; - private String resourceType = null; - private String format = null; - - private String transformation = null; - - private Boolean secure = null; - private Boolean cdnSubdomain = null; - - private String namedTransformation = null; - - /** stores the dynamic attributes */ - private Map tagAttrs = new HashMap(); - - public void doTag() throws JspException, IOException { - Cloudinary cloudinary = Singleton.getCloudinary(); - if (cloudinary == null) { - throw new JspException("Cloudinary config could not be located"); - } - - JspWriter out = getJspContext().getOut(); - - Map attributes = new HashMap(); + + protected Map prepareAttributes() { + Map attributes = new HashMap(); if (id != null) { attributes.put("id", id); } if (extraClasses != null) { attributes.put("class", extraClasses); } - - Url url = cloudinary.url(); - if (storedSrc != null) { - url.source(storedSrc); - } else { - url.source(src); - } - Transformation baseTransformation = new Transformation().params(tagAttrs); - if (null != namedTransformation && !namedTransformation.isEmpty()) baseTransformation.named(namedTransformation); - if (null == transformation || transformation.isEmpty()) { - url.transformation(baseTransformation); - } else { - url.transformation(baseTransformation.chain().rawTransformation(transformation)); - } - if (format != null) url.format(format); - if (type != null) url.type(type); - if (resourceType != null) url.resourceType(resourceType); - - if (secure != null) { - url.secure(secure.booleanValue()); - } else if(Boolean.TRUE.equals(isSecureRequest())) { - url.secure(true); - } - if (cdnSubdomain != null) url.cdnSubdomain(cdnSubdomain.booleanValue()); - - out.println(url.imageTag(attributes)); + return attributes; + } + + public void doTag() throws JspException, IOException { + JspWriter out = getJspContext().getOut(); + Url url = this.prepareUrl(); + out.println(url.imageTag(prepareAttributes())); } public void setId(String id) { @@ -106,98 +68,4 @@ public String getExtraClasses() { public void setExtraClasses(String extraClasses) { this.extraClasses = extraClasses; } - - public void setSrc(String src) { - this.src = src; - } - - public StoredFile getStoredSrc() { - return storedSrc; - } - - public void setStoredSrc(StoredFile storedSrc) { - this.storedSrc = storedSrc; - } - - public String getSrc() { - return src; - } - - @Deprecated - public void setPublicId(String src) { - this.src = src; - } - - @Deprecated - public String getPublicId() { - return src; - } - - public void setFormat(String format) { - this.format = format; - } - - public String getFormat() { - return format; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getResourceType() { - return resourceType; - } - - public void setResourceType(String resourceType) { - this.resourceType = resourceType; - } - - public String getTransformation() { - return transformation; - } - - public void setTransformation(String transformation) { - this.transformation = transformation.replaceAll("\\s+","/"); - } - - public Boolean getSecure() { - return secure; - } - - public void setSecure(Boolean secure) { - this.secure = secure; - } - - public Boolean getCdnSubdomain() { - return cdnSubdomain; - } - - public void setCdnSubdomain(Boolean cdnSubdomain) { - this.cdnSubdomain = cdnSubdomain; - } - - public String getNamed() { - return namedTransformation; - } - - public void setNamed(String namedTransformation) { - this.namedTransformation = namedTransformation; - } - - @Override - public void setDynamicAttribute(String uri, String name, Object value) throws JspException { - tagAttrs.put(name, value); - } - - private Boolean isSecureRequest() { - PageContext context = (PageContext) getJspContext(); - if (context == null) return null; - ServletRequest request = context.getRequest(); - return request.getScheme().equals("https"); - } } \ No newline at end of file diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsConfigTag.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsConfigTag.java index e181aa30..53155db4 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsConfigTag.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsConfigTag.java @@ -1,14 +1,16 @@ package com.cloudinary.taglib; -import com.cloudinary.Cloudinary; -import com.cloudinary.Singleton; +import java.io.IOException; import javax.servlet.jsp.JspException; import javax.servlet.jsp.JspWriter; import javax.servlet.jsp.tagext.SimpleTagSupport; -import java.io.IOException; -public class CloudinaryJsConfigTag extends SimpleTagSupport { +import com.cloudinary.Cloudinary; +import com.cloudinary.Singleton; + +public class CloudinaryJsConfigTag extends SimpleTagSupport { + @SuppressWarnings("unused") public void doTag() throws JspException, IOException { Cloudinary cloudinary = Singleton.getCloudinary(); if (cloudinary == null) { @@ -16,18 +18,20 @@ public void doTag() throws JspException, IOException { } JspWriter out = getJspContext().getOut(); out.println(""); } + + private void print(JspWriter out, String key, Object value) throws IOException { + if (value instanceof Boolean) { + out.println(key + ": " + ((Boolean) value ? "true" : "false") + ","); + } else { + out.println(key + ": \"" + value + "\","); + } + } } diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsIncludeTag.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsIncludeTag.java index 1905bb25..926f20eb 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsIncludeTag.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryJsIncludeTag.java @@ -24,7 +24,7 @@ public void doTag() throws JspException, IOException { } if (full) { String[] fullFiles = {"canvas-to-blob.min.js", "load-image.min.js", "jquery.fileupload-process.js", "uery.fileupload-image.js", "jquery.fileupload-validate.js"}; - for (String file : basicFiles) { + for (String file : fullFiles) { out.println(""); } } diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUnsignedUploadTag.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUnsignedUploadTag.java index ddd93e5f..4883f1d0 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUnsignedUploadTag.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUnsignedUploadTag.java @@ -10,6 +10,7 @@ public CloudinaryUnsignedUploadTag() { this.unsigned = true; } + @SuppressWarnings({ "unchecked", "rawtypes" }) protected String uploadTag(Uploader uploader, Map options, Map htmlOptions) { return uploader.unsignedImageUploadTag(fieldName, uploadPreset, options, htmlOptions); } diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUploadTag.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUploadTag.java index 6237b8b5..6b065865 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUploadTag.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUploadTag.java @@ -1,14 +1,20 @@ package com.cloudinary.taglib; import java.io.IOException; -import java.util.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.jsp.JspException; import javax.servlet.jsp.PageContext; import javax.servlet.jsp.tagext.SimpleTagSupport; -import com.cloudinary.*; +import com.cloudinary.Cloudinary; +import com.cloudinary.Singleton; +import com.cloudinary.Transformation; +import com.cloudinary.Uploader; public class CloudinaryUploadTag extends SimpleTagSupport { @@ -54,13 +60,14 @@ public class CloudinaryUploadTag extends SimpleTagSupport { private Boolean overwrite = null; private Boolean phash = null; protected boolean unsigned = false; + private Boolean mediaMetadata = null; public void doTag() throws JspException, IOException { Cloudinary cloudinary = Singleton.getCloudinary(); if (cloudinary == null) { throw new JspException("Cloudinary config could not be located"); } - Uploader uploader = cloudinary.uploader(); + Uploader uploader = (Uploader)cloudinary.uploader(); Map htmlOptions = new HashMap(); htmlOptions.put("type", "file"); @@ -86,6 +93,7 @@ public void doTag() throws JspException, IOException { options.put("faces", faces); options.put("colors", colors); options.put("image_metadata", imageMetadata); + options.put("media_metadata", mediaMetadata); options.put("use_filename", useFilename); options.put("unique_filename", uniqueFilename); options.put("eager_async", eagerAsync); @@ -415,13 +423,15 @@ public void setUploadPreset(String uploadPreset) { this.uploadPreset = uploadPreset; } + @SuppressWarnings({ "unchecked", "rawtypes" }) protected String uploadTag(Uploader uploader, Map options, Map htmlOptions) { return uploader.imageUploadTag(fieldName, options, htmlOptions); } - private void buildCallbackUrl(Map options) { + @SuppressWarnings({ "rawtypes", "unchecked" }) + private void buildCallbackUrl(Map options) { String callback = (String) options.get("callback"); - if (callback == null || callback.isEmpty()) callback = Singleton.getCloudinary().getStringConfig("callback"); + if (callback == null || callback.isEmpty()) callback = Singleton.getCloudinary().config.callback; if (callback == null || callback.isEmpty()) callback = "/cloudinary_cors.html"; if (!callback.matches("^https?://")) { PageContext context = (PageContext) getJspContext(); diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUrl.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUrl.java index 93eb12ce..1586a946 100644 --- a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUrl.java +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryUrl.java @@ -14,13 +14,15 @@ import com.cloudinary.*; /** - * - * http://res.cloudinary.com/test123/image/upload/c_crop,h_101,w_100/test + * Generates a cloudinary resource url + * + *
For example,
{@code } + * will produce
{@code http://res.cloudinary.com/test123/image/upload/c_crop,h_101,w_100/test} * */ public class CloudinaryUrl extends SimpleTagSupport implements DynamicAttributes { - private String src = null; + protected String src = null; private StoredFile storedSrc = null; private String type = null; @@ -31,21 +33,23 @@ public class CloudinaryUrl extends SimpleTagSupport implements DynamicAttributes private Boolean secure = null; private Boolean cdnSubdomain = null; - + private Boolean signed = null; + private Boolean useRootPath = null; + private Boolean secureCdnSubdomain = null; + private String namedTransformation = null; + private String urlSuffix = null; /** stores the dynamic attributes */ - private Map tagAttrs = new HashMap(); - - public void doTag() throws JspException, IOException { - Cloudinary cloudinary = Singleton.getCloudinary(); + protected Map tagAttrs = new HashMap(); + + protected Url prepareUrl() throws JspException { + Cloudinary cloudinary = Singleton.getCloudinary(); if (cloudinary == null) { throw new JspException("Cloudinary config could not be located"); } - - JspWriter out = getJspContext().getOut(); - - Url url = cloudinary.url(); + + Url url = cloudinary.url(); if (storedSrc != null) { url.source(storedSrc); } else { @@ -64,7 +68,16 @@ public void doTag() throws JspException, IOException { url.secure(true); } if (cdnSubdomain != null) url.cdnSubdomain(cdnSubdomain.booleanValue()); - + if (signed != null) url.signed(signed.booleanValue()); + if (useRootPath != null) url.useRootPath(useRootPath); + if (urlSuffix != null) url.suffix(urlSuffix); + if (secureCdnSubdomain != null) url.secureCdnSubdomain(secureCdnSubdomain); + return url; + } + + public void doTag() throws JspException, IOException { + JspWriter out = getJspContext().getOut(); + Url url = this.prepareUrl(); out.println(url.generate()); } @@ -133,6 +146,14 @@ public void setCdnSubdomain(Boolean cdnSubdomain) { this.cdnSubdomain = cdnSubdomain; } + public Boolean getSigned() { + return signed; + } + + public void setSigned(Boolean signed) { + this.signed = signed; + } + public String getNamed() { return namedTransformation; } @@ -152,4 +173,28 @@ private Boolean isSecureRequest() { ServletRequest request = context.getRequest(); return request.getScheme().equals("https"); } + + public Boolean getUseRootPath() { + return useRootPath; + } + + public void setUseRootPath(Boolean useRootPath) { + this.useRootPath = useRootPath; + } + + public Boolean getSecureCdnSubdomain() { + return secureCdnSubdomain; + } + + public void setSecureCdnSubdomain(Boolean secureCdnSubdomain) { + this.secureCdnSubdomain = secureCdnSubdomain; + } + + public String getUrlSuffix() { + return urlSuffix; + } + + public void setUrlSuffix(String urlSuffix) { + this.urlSuffix = urlSuffix; + } } diff --git a/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryVideoTag.java b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryVideoTag.java new file mode 100644 index 00000000..ec2adb54 --- /dev/null +++ b/cloudinary-taglib/src/main/java/com/cloudinary/taglib/CloudinaryVideoTag.java @@ -0,0 +1,110 @@ +package com.cloudinary.taglib; + +import java.io.IOException; +import java.util.Map; + +import javax.servlet.jsp.JspException; +import javax.servlet.jsp.JspWriter; + +import com.cloudinary.Transformation; +import com.cloudinary.Url; + +public class CloudinaryVideoTag extends CloudinaryImageTag { + private String sourceTypes; + private Object poster; + private Boolean autoplay; + private Boolean controls; + private Boolean loop; + private Boolean muted; + private Boolean preload; + + public Boolean getAutoplay() { + return autoplay; + } + public void setAutoplay(Boolean autoplay) { + this.autoplay = autoplay; + } + public Boolean getControls() { + return controls; + } + public void setControls(Boolean controls) { + this.controls = controls; + } + public Boolean getLoop() { + return loop; + } + public void setLoop(Boolean loop) { + this.loop = loop; + } + public Boolean getMuted() { + return muted; + } + public void setMuted(Boolean muted) { + this.muted = muted; + } + public Boolean getPreload() { + return preload; + } + public void setPreload(Boolean preload) { + this.preload = preload; + } + public Object getPoster() { + return poster; + } + public void setPoster(Object poster) { + this.poster = poster; + } + + public String getSourceTypes() { + return sourceTypes; + } + public void setSourceTypes(String sourceTypes) { + this.sourceTypes = sourceTypes; + } + + public void doTag() throws JspException, IOException { + JspWriter out = getJspContext().getOut(); + Url url = this.prepareUrl(); + + String sourceTypes[] = null; + if (this.sourceTypes != null) { + sourceTypes = this.sourceTypes.split(","); + url.sourceTypes(sourceTypes); + } + + if (this.poster != null) { + if (this.poster.equals("false")) { + url.poster(false); + } else { + url.poster(this.poster); + } + } + + Map attributes = prepareAttributes(); + + if (sourceTypes == null) sourceTypes = Url.DEFAULT_VIDEO_SOURCE_TYPES; + for (String sourceType : sourceTypes) { + String transformationAttribute = sourceType + "Transformation"; + if (this.tagAttrs.containsKey(transformationAttribute)) { + Transformation transformation = null; + Object transformationAttrValue = tagAttrs.remove(transformationAttribute); + if (transformationAttrValue instanceof Transformation) { + transformation = (Transformation) transformationAttrValue; + } else if (transformationAttrValue instanceof Map) { + transformation = new Transformation().params((Map) transformationAttrValue); + } else { + transformation = new Transformation().rawTransformation((String) transformationAttrValue); + } + url.sourceTransformationFor(sourceType, transformation ); + } + } + + if (autoplay != null) attributes.put("autoplay", autoplay.toString()); + if (controls != null) attributes.put("controls", controls.toString()); + if (loop != null) attributes.put("loop", loop.toString()); + if (muted != null) attributes.put("muted", muted.toString()); + if (preload != null) attributes.put("preload", preload.toString()); + + out.println(url.videoTag(attributes)); + } +} diff --git a/cloudinary-taglib/src/main/resources/META-INF/cloudinary.tld b/cloudinary-taglib/src/main/resources/META-INF/cloudinary.tld index 457f8133..0370c3bf 100644 --- a/cloudinary-taglib/src/main/resources/META-INF/cloudinary.tld +++ b/cloudinary-taglib/src/main/resources/META-INF/cloudinary.tld @@ -1,7 +1,7 @@ - + 1.1 2.0 Cloudinary Taglib @@ -341,11 +341,122 @@ false true + + signed + false + true + + + named + false + true + + true + + + video + com.cloudinary.taglib.CloudinaryVideoTag + scriptless + + id + false + true + + + extraClasses + false + true + + + src + false + true + + + storedSrc + false + true + + + publicId + false + true + + + format + false + true + + + type + false + true + + + resourceType + false + true + + + transformation + false + true + + + secure + false + true + + + cdnSubdomain + false + true + + + signed + false + true + named false true + + sourceTypes + false + true + + + poster + false + true + + + autoplay + false + true + + + controls + false + true + + + loop + false + true + + + muted + false + true + + + preload + false + true + true @@ -392,11 +503,31 @@ false true + + signed + false + true + named false true + + urlSuffix + false + true + + + secureCdnSubdomain + false + true + + + useRootPath + false + true + true @@ -419,4 +550,4 @@ true - \ No newline at end of file + diff --git a/cloudinary-test-common/build.gradle b/cloudinary-test-common/build.gradle new file mode 100644 index 00000000..e387870b --- /dev/null +++ b/cloudinary-test-common/build.gradle @@ -0,0 +1,17 @@ +plugins { + id 'java-library' +} + +apply from: "../java_shared.gradle" +apply from: "../publish.gradle" + +task ciTest( type: Test ) + +dependencies { + compile project(':cloudinary-core') + compile group: 'org.hamcrest', name: 'java-hamcrest', version: '2.0.0.0' + compile group: 'junit', name: 'junit', version: '4.12' + testCompile group: 'pl.pragmatists', name: 'JUnitParams', version: '1.0.5' +} + +// Publishing configuration moved to ../publish.gradle \ No newline at end of file diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractAccountApiTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractAccountApiTest.java new file mode 100644 index 00000000..7852f96b --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractAccountApiTest.java @@ -0,0 +1,556 @@ +package com.cloudinary.test; + + +import com.cloudinary.Cloudinary; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.provisioning.Account; +import com.cloudinary.utils.ObjectUtils; +import org.junit.*; +import org.junit.rules.ExpectedException; +import org.junit.rules.TestName; + +import java.util.*; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.*; + +public abstract class AbstractAccountApiTest extends MockableTest { + private static Random rand = new Random(); + protected Account account; + private static Set createdSubAccountIds = new HashSet(); + private static Set createdUserIds = new HashSet(); + private static Set createdGroupIds = new HashSet(); + + @BeforeClass + public static void setUpClass() { + + } + + @Rule + public TestName currentTest = new TestName(); + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Before + public void setUp() throws Exception { + assumeCloudinaryAccountURLExist(); + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.account = new Account(new Cloudinary()); + } + + @AfterClass + public static void tearDownClass() { + assumeCloudinaryAccountURLExist(); + System.out.println("Start TearDownClass"); + Account account = new Account(new Cloudinary()); + for (String createdSubAccountId : createdSubAccountIds) { + try { + account.deleteSubAccount(createdSubAccountId, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + for (String userId : createdUserIds) { + try { + account.deleteUser(userId, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + + for (String groupId : createdGroupIds) { + try { + account.deleteUserGroup(groupId, null); + } catch (Exception e) { + e.printStackTrace(); + } + } + System.out.println("### Deleted - SubAccounts:"+createdSubAccountIds.size()+", Users:"+createdUserIds.size()+ ", UserGroups:"+createdGroupIds.size()); + } + + @Test + public void testPassingCredentialsThroughOptions() throws Exception { + assumeCloudinaryAccountURLExist(); + int exceptions = 0; + + Map map = singletonMap("provisioning_api_secret", new Object()) ; + try { + this.account.subAccounts(true, null, null, map); + } catch (IllegalArgumentException ignored){ + exceptions++; + } + + map = singletonMap("provisioning_api_key", new Object()) ; + try { + this.account.subAccounts(true, null, null, map); + } catch (IllegalArgumentException ignored){ + exceptions++; + } + + map = new HashMap(); + map.put("provisioning_api_key", "abc"); + map.put("provisioning_api_secret", "def"); + + try { + this.account.subAccounts(true, null, null, map); + } catch (Exception ex){ + assertTrue(ex.getMessage().contains("Invalid credentials")); + exceptions++; + } + + assertEquals(3, exceptions); + } + + // Sub accounts tests + @Test + public void testGetSubAccount() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse accountResponse = createSubAccount(); + ApiResponse account = this.account.subAccount(accountResponse.get("id").toString(), null); + assertNotNull(account); + } + + @Test + public void testGetSubAccounts() throws Exception { + assumeCloudinaryAccountURLExist(); + createSubAccount(); + ApiResponse accounts = account.subAccounts(null, null, null, null); + assertNotNull(accounts); + assertTrue(((ArrayList) accounts.get("sub_accounts")).size() >= 1); + } + + @Test + public void testCreateSubAccount() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse result = createSubAccount(); + assertNotNull(result); + + String message = ""; + try { + // test that the parameters are passed correctly - throws exception since the from-account id doesn't exist: + account.createSubAccount(randomLetters(), null, emptyMap(), true, "non-existing-id", null); + } catch (Exception ex){ + message = ex.getMessage(); + } + + assertTrue(message.contains("cannot find sub account")); + } + + @Test + public void testUpdateSubAccount() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse subAccount = createSubAccount(); + String newCloudName = randomLetters(); + ApiResponse result = account.updateSubAccount(subAccount.get("id").toString(), null, newCloudName, Collections.emptyMap(), null, null); + assertNotNull(result); + assertEquals(result.get("cloud_name"), newCloudName); + } + + @Test + public void testDeleteSubAccount() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse createResult = createSubAccount(); + String id = createResult.get("id").toString(); + ApiResponse result = account.deleteSubAccount(id, null); + assertNotNull(result); + assertEquals(result.get("message"), "ok"); + createdSubAccountIds.remove(id); + } + + // Users test + @Test + public void testGetUser() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(); + String userId = user.get("id").toString(); + ApiResponse result = account.user(userId, null); + + assertNotNull(result); + deleteUser(userId); + } + + @Test + public void testGetUsers() throws Exception { + assumeCloudinaryAccountURLExist(); + String user1Id = createUser(Account.Role.MASTER_ADMIN).get("id").toString(); + String user2Id = createUser(Account.Role.MASTER_ADMIN).get("id").toString(); + ApiResponse result = account.users(null, Arrays.asList(user1Id, user2Id), null, null, null); + assertNotNull(result); + final ArrayList users = (ArrayList) result.get("users"); + ArrayList returnedIds = new ArrayList(2); + + assertEquals("Should return two users", 2, users.size()); + + returnedIds.add(((Map) users.get(0)).get("id").toString()); + returnedIds.add(((Map) users.get(1)).get("id").toString()); + + assertTrue("User1 id should be in the result set", returnedIds.contains(user1Id)); + assertTrue("User2 id should be in the result set", returnedIds.contains(user2Id)); + deleteUser(user1Id); + deleteUser(user2Id); + } + + @Test + public void testGetPendingUsers() throws Exception { + assumeCloudinaryAccountURLExist(); + String id = createUser(Account.Role.BILLING).get("id").toString(); + + ApiResponse pending = account.users(true, Collections.singletonList(id), null, null, null); + assertEquals(1, ((ArrayList) pending.get("users")).size()); + + ApiResponse notPending = account.users(false, Collections.singletonList(id), null, null, null); + assertEquals(0, ((ArrayList) notPending.get("users")).size()); + + ApiResponse all = account.users(null, Collections.singletonList(id), null, null, null); + assertEquals(1, ((ArrayList) all.get("users")).size()); + } + + @Test + public void testGetUsersByPrefix() throws Exception { + assumeCloudinaryAccountURLExist(); + final long timeMillis = System.currentTimeMillis(); + final String userName = String.format("SDK TEST Get Users By Prefix %d", timeMillis); + final String userEmail = String.format("sdk-test-get-users-by-prefix+%d@cloudinary.com", timeMillis); + + createUser(userName, + userEmail, + Account.Role.BILLING, + Collections.emptyList()); + + ApiResponse userByPrefix = account.users(true, null, userName.substring(0, userName.length() - 1), null, null); + assertEquals(1, ((ArrayList) userByPrefix.get("users")).size()); + + ApiResponse userByNonExistingPrefix = account.users(true, null, userName + "zzz", null, null); + assertEquals(0, ((ArrayList) userByNonExistingPrefix.get("users")).size()); + } + + @Test + public void testGetUsersBySubAccountIds() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse subAccount = createSubAccount(); + final String subAccountId = subAccount.get("id").toString(); + + final long timeMillis = System.currentTimeMillis(); + final String userName = String.format("SDK TEST Get Users By Sub Account Ids %d", timeMillis); + final String userEmail = String.format("sdk-test-get-users-by-sub-account-ids+%d@cloudinary.com", timeMillis); + + createUser(userName, + userEmail, + Account.Role.BILLING, + Collections.singletonList(subAccountId)); + + ApiResponse usersBySubAccount = account.users(true, null, userName, subAccountId, null); + assertEquals(1, ((ArrayList) usersBySubAccount.get("users")).size()); + } + + @Test + public void testGetUsersThrowsWhenSubAccountIdDoesntExist() throws Exception { + assumeCloudinaryAccountURLExist(); + final String subAccountId = randomLetters(); + expectedException.expectMessage("Cannot find sub account with id " + subAccountId); + account.users(true, null, null, subAccountId, null); + } + + @Test + public void testCreateUser() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse createResult = createSubAccount(); + ApiResponse result = createUser(Collections.singletonList(createResult.get("id").toString())); + assertNotNull(result); + } + + @Test + public void testCreateUserWithOptions() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse createResult = createSubAccount(); + ApiResponse result = createUser(Collections.singletonList(createResult.get("id").toString()), ObjectUtils.emptyMap()); + assertNotNull(result); + } + + @Test + public void testCreateUserEnabled() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse createResult = createSubAccount(); + ApiResponse result = createUser(Collections.singletonList(createResult.get("id").toString()), true); + assertTrue((Boolean) result.get("enabled")); + } + + @Test + public void testCreateUserDisabled() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse createResult = createSubAccount(); + ApiResponse result = createUser(Collections.singletonList(createResult.get("id").toString()), false); + assertFalse((Boolean) result.get("enabled")); + } + + @Test + public void testUpdateUser() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(Account.Role.ADMIN); + String userId = user.get("id").toString(); + String newName = randomLetters(); + ApiResponse result = account.updateUser(userId, newName, null, null, null, null); + + assertNotNull(result); + assertEquals(result.get("name"), newName); + deleteUser(userId); + } + + @Test + public void testUpdateUserEnabled() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(Account.Role.ADMIN); + String userId = user.get("id").toString(); + String newName = randomLetters(); + ApiResponse result = account.updateUser(userId, newName, null, null, true, null, null); + + assertNotNull(result); + assertTrue((Boolean) result.get("enabled")); + deleteUser(userId); + } + + @Test + public void testUpdateUserDisabled() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(Account.Role.ADMIN); + String userId = user.get("id").toString(); + String newName = randomLetters(); + ApiResponse result = account.updateUser(userId, newName, null, null, false, null, null); + + assertNotNull(result); + assertFalse((Boolean) result.get("enabled")); + deleteUser(userId); + } + + @Test + public void testDeleteUser() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(Collections.emptyList()); + String id = user.get("id").toString(); + ApiResponse result = account.deleteUser(id, null); + assertEquals(result.get("message"), "ok"); + createdUserIds.remove(id); + } + + // groups + @Test + public void testCreateUserGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse group = createGroup(); + assertNotNull(group); + } + + @Test + public void testUpdateUserGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse group = createGroup(); + String newName = randomLetters(); + ApiResponse result = account.updateUserGroup(group.get("id").toString(), newName, null); + assertNotNull(result); + } + + @Test + public void testDeleteUserGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse group = createGroup(); + String id = group.get("id").toString(); + ApiResponse result = account.deleteUserGroup(id, null); + assertNotNull(result); + assertEquals(result.get("ok"), true); + createdGroupIds.remove(id); + } + + @Test + public void testAddUserToUserGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(); + ApiResponse group = createGroup(); + String userId = user.get("id").toString(); + ApiResponse result = account.addUserToGroup(group.get("id").toString(), userId, null); + assertNotNull(result); + deleteUser(userId); + } + + @Test + public void testRemoveUserFromUserGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = createUser(Account.Role.MEDIA_LIBRARY_ADMIN); + ApiResponse group = createGroup(); + String groupId = group.get("id").toString(); + String userId = user.get("id").toString(); + account.addUserToGroup(groupId, userId, null); + ApiResponse result = account.removeUserFromGroup(groupId, userId, null); + assertNotNull(result); + deleteUser(userId); + } + + @Test + public void testListUserGroups() throws Exception { + assumeCloudinaryAccountURLExist(); + createGroup(); + ApiResponse result = account.userGroups(); + assertNotNull(result); + assertTrue(((List) result.get("user_groups")).size() >= 1); + } + + @Test + public void testListUserGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse group = createGroup(); + ApiResponse result = account.userGroup(group.get("id").toString(), null); + assertNotNull(result); + } + + @Test + public void testListUsersInGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user1 = createUser(); + ApiResponse user2 = createUser(); + ApiResponse group = createGroup(); + String groupId = group.get("id").toString(); + String user1Id = user1.get("id").toString(); + String user2Id = user2.get("id").toString(); + account.addUserToGroup(groupId, user1Id, null); + account.addUserToGroup(groupId, user2Id, null); + ApiResponse result = account.userGroupUsers(groupId, null); + assertNotNull(result); + assertTrue(((List) result.get("users")).size() >= 2); + deleteUser(user1Id); + deleteUser(user2Id); + } + + @Test + public void testGetAccessKeys() throws Exception { + ApiResponse createResult = createSubAccount(); + ApiResponse result = account.getAccessKeys((String) createResult.get("id"), ObjectUtils.emptyMap()); + assertNotNull(result); + } + + @Test + public void testCreateNewAccessKey() throws Exception { + ApiResponse createResult = createSubAccount(); + String name = randomLetters(); + ApiResponse result = account.createAccessKey((String)createResult.get("id"), name, true, ObjectUtils.emptyMap()); + assertNotNull(result); + assertTrue((Boolean) result.get("enabled")); + } + + @Test + public void testUpdateAccessKey() throws Exception { + ApiResponse createResult = createSubAccount(); + String name = randomLetters(); + ApiResponse result = account.createAccessKey((String)createResult.get("id"), name, false, ObjectUtils.emptyMap()); + assertNotNull(result); + + String updatedName = randomLetters(); + result = account.updateAccessKey((String)createResult.get("id"), (String) result.get("api_key"), updatedName, true, ObjectUtils.emptyMap()); + assertNotNull(result); + assertEquals(updatedName, result.get("name")); + assertTrue((Boolean) result.get("enabled")); + } + + @Test + public void testDeleteAccessKey() throws Exception { + ApiResponse createResult = createSubAccount(); + String name = randomLetters(); + ApiResponse result = account.createAccessKey((String)createResult.get("id"), name, false, ObjectUtils.emptyMap()); + assertNotNull(result); + + result = account.deleteAccessKey((String)createResult.get("id"), (String) result.get("api_key"), ObjectUtils.emptyMap()); + assertNotNull(result); + } + + + // Helpers + private ApiResponse createGroup() throws Exception { + assumeCloudinaryAccountURLExist(); + String name = randomLetters(); + ApiResponse userGroup = account.createUserGroup(name); + createdGroupIds.add(userGroup.get("id").toString()); + return userGroup; + } + + private ApiResponse createUser() throws Exception { + assumeCloudinaryAccountURLExist(); + return createUser(Collections.emptyList()); + } + + private ApiResponse createUser(Account.Role role) throws Exception { + assumeCloudinaryAccountURLExist(); + return createUser(Collections.emptyList(), role); + } + + private ApiResponse createUser(List subAccountsIds) throws Exception { + assumeCloudinaryAccountURLExist(); + return createUser(subAccountsIds, Account.Role.BILLING); + } + + private ApiResponse createUser(List subAccountsIds, Map options) throws Exception { + assumeCloudinaryAccountURLExist(); + return createUser(subAccountsIds, Account.Role.BILLING, options); + } + + private ApiResponse createUser(List subAccountsIds, Boolean enabled) throws Exception { + assumeCloudinaryAccountURLExist(); + return createUser(subAccountsIds, Account.Role.BILLING, enabled); + } + + private ApiResponse createUser(List subAccountsIds, Account.Role role) throws Exception { + assumeCloudinaryAccountURLExist(); + String email = "sdk+" + SDK_TEST_TAG + randomLetters() + "@cloudinary.com"; + return createUser("TestName", email, role, subAccountsIds); + } + + private ApiResponse createUser(List subAccountsIds, Account.Role role, Map options) throws Exception { + assumeCloudinaryAccountURLExist(); + String email = "sdk+" + SDK_TEST_TAG + randomLetters() + "@cloudinary.com"; + ApiResponse user = account.createUser("TestUserJava"+new Date().toString(), email, role, null, subAccountsIds, options); + createdUserIds.add(user.get("id").toString()); + return user; + } + + private ApiResponse createUser(List subAccountsIds, Account.Role role, Boolean enabled) throws Exception { + assumeCloudinaryAccountURLExist(); + String email = "sdk+" + SDK_TEST_TAG + randomLetters() + "@cloudinary.com"; + ApiResponse user = account.createUser("TestUserJava"+new Date().toString(), email, role, enabled, subAccountsIds, null); + createdUserIds.add(user.get("id").toString()); + return user; + } + + private ApiResponse createUser(final String name, String email, Account.Role role, List subAccountsIds) throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse user = account.createUser(name, email, role, subAccountsIds, null); + createdUserIds.add(user.get("id").toString()); + return user; + } + + private void deleteUser(String userId){ + assumeCloudinaryAccountURLExist(); + try { + account.deleteUser(userId, null); + createdUserIds.remove(userId); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + private ApiResponse createSubAccount() throws Exception { + assumeCloudinaryAccountURLExist(); + ApiResponse subAccount = account.createSubAccount(randomLetters(), null, emptyMap(), true, null); + createdSubAccountIds.add(subAccount.get("id").toString()); + return subAccount; + } + + private static String randomLetters() { + assumeCloudinaryAccountURLExist(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 10; i++) { + sb.append((char) ('a' + rand.nextInt('z' - 'a' + 1))); + } + return sb.toString(); + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractApiTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractApiTest.java new file mode 100644 index 00000000..90e90fa7 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractApiTest.java @@ -0,0 +1,1406 @@ +package com.cloudinary.test; + +import com.cloudinary.*; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.exceptions.BadRequest; +import com.cloudinary.api.exceptions.NotFound; +import com.cloudinary.test.helpers.Feature; +import com.cloudinary.test.rules.RetryRule; +import com.cloudinary.transformation.TextLayer; +import com.cloudinary.utils.ObjectUtils; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.*; + +import static com.cloudinary.utils.ObjectUtils.asMap; +import static com.cloudinary.utils.ObjectUtils.emptyMap; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.core.AllOf.allOf; +import static org.hamcrest.core.IsNot.not; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +@SuppressWarnings({"rawtypes", "unchecked", "JavaDoc"}) +abstract public class AbstractApiTest extends MockableTest { + private static final String API_TEST = "api_test_" + SUFFIX; + private static final String API_TEST_1 = API_TEST + "_1"; + private static final String API_TEST_2 = API_TEST + "_2"; + private static final String API_TEST_3 = API_TEST + "_3"; + private static final String API_TEST_5 = API_TEST + "_5"; + public static final String API_TEST_TRANSFORMATION = "api_test_transformation_" + SUFFIX; + public static final String API_TEST_TRANSFORMATION_2 = API_TEST_TRANSFORMATION + "2"; + public static final String API_TEST_TRANSFORMATION_3 = API_TEST_TRANSFORMATION + "3"; + public static final String API_TEST_UPLOAD_PRESET = "api_test_upload_preset_" + SUFFIX; + public static final String API_TEST_UPLOAD_PRESET_2 = API_TEST_UPLOAD_PRESET + "2"; + public static final String API_TEST_UPLOAD_PRESET_3 = API_TEST_UPLOAD_PRESET + "3"; + public static final String API_TEST_UPLOAD_PRESET_4 = API_TEST_UPLOAD_PRESET + "4"; + public static final String API_TAG = SDK_TEST_TAG + "_api"; + public static final String DIRECTION_TAG = SDK_TEST_TAG + "_api_resource_direction"; + public static final String[] UPLOAD_TAGS = {SDK_TEST_TAG, API_TAG}; + public static final String EXPLICIT_TRANSFORMATION_NAME = "c_scale,l_text:Arial_60:" + SUFFIX + ",w_100"; + public static final Transformation EXPLICIT_TRANSFORMATION = new Transformation().width(100).crop("scale").overlay(new TextLayer().text(SUFFIX).fontFamily("Arial").fontSize(60)); + public static final String UPDATE_TRANSFORMATION_NAME = "c_scale,l_text:Arial_60:" + SUFFIX + "_update,w_100"; + public static final Transformation UPDATE_TRANSFORMATION = new Transformation().width(100).crop("scale").overlay(new TextLayer().text(SUFFIX + "_update").fontFamily("Arial").fontSize(60)); + public static final String DELETE_TRANSFORMATION_NAME = "c_scale,l_text:Arial_60:" + SUFFIX + "_delete,w_100"; + public static final Transformation DELETE_TRANSFORMATION = new Transformation().width(100).crop("scale").overlay(new TextLayer().text(SUFFIX + "_delete").fontFamily("Arial").fontSize(60)); + public static final String TEST_KEY = "test-key" + SUFFIX; + public static final String API_TEST_RESTORE = "api_test_restore" + SUFFIX; + public static final Set createdFolders = new HashSet(); + private static final String CUSTOM_USER_AGENT_PREFIX = "TEST_USER_AGENT"; + private static final String CUSTOM_USER_AGENT_VERSION = "9.9.9"; + private static String assetId1; + private static String assetId2; + private static String assetId3; + + private static final int SLEEP_TIMEOUT = 5000; + + + protected Api api; + + @BeforeClass + public static void setUpClass() throws IOException { + Cloudinary cloudinary = new Cloudinary(); + if (cloudinary.config.apiSecret == null) { + System.err.println("Please setup environment for Upload test to run"); + return; + } + + List uploadAndDirectionTag = new ArrayList(Arrays.asList(UPLOAD_TAGS)); + uploadAndDirectionTag.add(DIRECTION_TAG); + + Map options = ObjectUtils.asMap("public_id", API_TEST, "tags", uploadAndDirectionTag, "context", "key=value", "eager", + Collections.singletonList(EXPLICIT_TRANSFORMATION)); + assetId1 = cloudinary.uploader().upload(SRC_TEST_IMAGE, options).get("asset_id").toString(); + + options.put("public_id", API_TEST_1); + assetId2 = cloudinary.uploader().upload(SRC_TEST_IMAGE, options).get("asset_id").toString(); + options.remove("public_id"); + + assetId3 = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("asset_folder", "test_asset_folder")).get("public_id").toString(); + + options.put("eager", Collections.singletonList(UPDATE_TRANSFORMATION)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + + options.put("eager", Collections.singletonList(DELETE_TRANSFORMATION)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + + String context1 = TEST_KEY + "=alt"; + String context2 = TEST_KEY + "=alternate"; + + options = ObjectUtils.asMap("public_id", "context_1" + SUFFIX, "tags", uploadAndDirectionTag, "context", context1); + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + + options = ObjectUtils.asMap("public_id", "context_2" + SUFFIX, "tags", uploadAndDirectionTag, "context", context2); + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + } + + @AfterClass + public static void tearDownClass() { + Api api = new Cloudinary().api(); + try { + api.deleteResourcesByTag(API_TAG, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteTransformation(API_TEST_TRANSFORMATION, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteTransformation(API_TEST_TRANSFORMATION_2, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteTransformation(API_TEST_TRANSFORMATION_3, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET_2, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET_3, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET_4, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + for (String folder : createdFolders) { + api.deleteFolder(folder, ObjectUtils.emptyMap()); + } + } catch (Exception ignored) { + } + } + + @Rule + public TestName currentTest = new TestName(); + + @Rule + public RetryRule retryRule = new RetryRule(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + assumeNotNull(cloudinary.config.apiSecret); + this.api = cloudinary.api(); + + + } + + public Map findByAttr(List elements, String attr, Object value) { + for (Map element : elements) { + if (value.equals(element.get(attr))) { + return element; + } + } + return null; + } + + @Test + public void testCustomUserAgent() throws Exception { + // should allow setting a custom user-agent + cloudinary.setUserAgent(CUSTOM_USER_AGENT_PREFIX, CUSTOM_USER_AGENT_VERSION); + Map results = api.ping(ObjectUtils.emptyMap()); + //TODO Mock server and assert the header + } + + @Test + public void test01ResourceTypes() throws Exception { + // should allow listing resource_types + Map result = api.resourceTypes(ObjectUtils.emptyMap()); + final List resource_types = (List) result.get("resource_types"); + assertThat(resource_types, hasItem("image")); + } + + @Test + public void testSingleSelectiveResponse() throws Exception { + Map options = new HashMap(); + options.put("fields", "width"); + Map result = api.resources(options); + List resources = (List) result.get("resources"); + assertNotNull(resources); + Map resource = resources.get(0); + assertNotNull(resource); + assertNotNull(resource.get("width")); + assertNull(resource.get("format")); + } + + @Test + public void testMultipleSelectiveResponse() throws Exception { + Map options = new HashMap(); + options.put("fields", new String[]{"width", "format"}); + Map result = api.resources(options); + List resources = (List) result.get("resources"); + assertNotNull(resources); + Map resource = resources.get(0); + assertNotNull(resource); + assertNotNull(resource.get("width")); + assertNotNull(resource.get("format")); + assertNull(resource.get("height")); + } + + @Test + public void test03ResourcesCursor() throws Exception { + // should allow listing resources with cursor + Map options = new HashMap(); + options.put("max_results", 1); + Map result = api.resources(options); + List resources = (List) result.get("resources"); + assertNotNull(resources); + assertEquals(1, resources.size()); + assertNotNull(result.get("next_cursor")); + + options.put("next_cursor", result.get("next_cursor")); + Map result2 = api.resources(options); + List resources2 = (List) result2.get("resources"); + assertNotNull(resources2); + assertEquals(resources2.size(), 1); + assertNotSame(resources2.get(0).get("public_id"), resources.get(0).get("public_id")); + } + + @Test + public void test04ResourcesByType() throws Exception { + // should allow listing resources by type + Map result = api.resources(ObjectUtils.asMap("type", "upload", "max_results", 10)); + List resources = (List) result.get("resources"); + + // beforeClass hook uploads several type:upload resources, we can rely on it. + assertTrue(resources.size() > 0); + } + + @Test + public void testOAuthToken() { + String message = ""; + try { + api.resource(API_TEST, Collections.singletonMap("oauth_token", "not_a_real_token")); + } catch (Exception e) { + message = e.getMessage(); + } + + assertTrue(message.contains("Invalid token")); + } + + @Test + public void test05ResourcesByPrefix() throws Exception { + // should allow listing resources by prefix + Map result = api.resources(ObjectUtils.asMap("type", "upload", "prefix", API_TEST, "tags", true, "context", true)); + List resources = (List) result.get("resources"); + assertThat(resources, hasItem(hasEntry("public_id", (Object) API_TEST))); + assertThat(resources, hasItem(hasEntry("public_id", (Object) API_TEST_1))); +// resources = (List>) result.get("resources"); + assertThat(resources, hasItem(allOf(hasEntry("public_id", API_TEST), hasEntry("type", "upload")))); + assertThat(resources, hasItem(hasEntry("context", ObjectUtils.asMap("custom", ObjectUtils.asMap("key", "value"))))); + assertThat(resources, hasItem(hasEntry(equalTo("tags"), hasItem(API_TAG)))); + } + + @Test + public void testResourcesListingDirection() throws Exception { + // should allow listing resources in both directions + Map result = api.resourcesByTag(DIRECTION_TAG, ObjectUtils.asMap("type", "upload", "direction", "asc", "max_results", 500)); + List resources = (List) result.get("resources"); + ArrayList resourceIds = new ArrayList(); + for (Map resource : resources) { + resourceIds.add((String) resource.get("public_id")); + } + result = api.resourcesByTag(DIRECTION_TAG, ObjectUtils.asMap("type", "upload", "direction", -1, "max_results", 500)); + List resourcesDesc = (List) result.get("resources"); + ArrayList resourceIdsDesc = new ArrayList(); + for (Map resource : resourcesDesc) { + resourceIdsDesc.add((String) resource.get("public_id")); + } + Collections.reverse(resourceIds); + assertEquals(resourceIds, resourceIdsDesc); + } + + @Ignore + public void testResourcesListingStartAt() throws Exception { + // should allow listing resources by start date - make sure your clock + // is set correctly!!! + Thread.sleep(2000L); + java.util.Date startAt = new java.util.Date(); + Thread.sleep(2000L); + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + ApiResponse listResources = api.resources(ObjectUtils.asMap("type", "upload", "start_at", startAt, "direction", "asc")); + List resources = (List) listResources.get("resources"); + assertEquals(response.get("public_id"), resources.get(0).get("public_id")); + } + + @Test + public void testTransformationsWithCursor() throws Exception { + String name = "testTransformation" + SDK_TEST_TAG + System.currentTimeMillis(); + api.createTransformation(name, "c_scale,w_100", null); + final List transformations = new ArrayList(); + String next_cursor = null; + do { + Map result = api.transformations(ObjectUtils.asMap("max_results", 500, "next_cursor", next_cursor)); + transformations.addAll((List) result.get("transformations")); + next_cursor = (String) result.get("next_cursor"); + } while (next_cursor != null); + assertThat(transformations, hasItem(allOf(hasEntry("name", "t_" + name)))); + } + + @Test + public void testResourcesByAssetIds() throws Exception { + Map result = api.resourcesByAssetIDs(Arrays.asList(assetId1, assetId2), ObjectUtils.asMap("tags", true, "context", true)); + List resources = (List) result.get("resources"); + assertEquals(2, resources.size()); + assertNotNull(findByAttr(resources, "public_id", API_TEST)); + assertNotNull(findByAttr(resources, "public_id", API_TEST_1)); + } + + @Test + public void testResourceByAssetId() throws Exception { + Map result = api.resourceByAssetID(assetId1, ObjectUtils.asMap("tags", true, "context", true)); + assertEquals(API_TEST, result.get("public_id").toString()); + } + + @Test + public void testResourceByAssetFolder() throws Exception { + if (MockableTest.shouldTestFeature(Feature.DYNAMIC_FOLDERS)) { + Map result = api.resourcesByAssetFolder("test_asset_folder", ObjectUtils.asMap("tags", true, "context", true)); + assertNotNull(findByAttr((List) result.get("resources"), "public_id", assetId3)); + } + } + + @Test + public void testResourcesByPublicIds() throws Exception { + // should allow listing resources by public ids + Map result = api.resourcesByIds(Arrays.asList(API_TEST, API_TEST_1, "bogus"), ObjectUtils.asMap("type", "upload", "tags", true, "context", true)); + List resources = (List) result.get("resources"); + assertEquals(2, resources.size()); + assertNotNull(findByAttr(resources, "public_id", API_TEST)); + assertNotNull(findByAttr(resources, "public_id", API_TEST_1)); + assertNotNull(findByAttr((List) result.get("resources"), "context", ObjectUtils.asMap("custom", ObjectUtils.asMap("key", "value")))); + boolean found = false; + for (Map r : resources) { + ArrayList tags = (ArrayList) r.get("tags"); + found = found || tags.contains(API_TAG); + } + assertTrue(found); + } + + @Test + public void test06ResourcesTag() throws Exception { + // should allow listing resources by tag + Map result = api.resourcesByTag(API_TAG, ObjectUtils.asMap("tags", true, "context", true, "max_results", 500)); + Map resource = findByAttr((List) result.get("resources"), "public_id", API_TEST); + assertNotNull(resource); + resource = findByAttr((List) result.get("resources"), "context", ObjectUtils.asMap("custom", ObjectUtils.asMap("key", "value"))); + assertNotNull(resource); + List resources = (List) result.get("resources"); + boolean found = false; + for (Map r : resources) { + ArrayList tags = (ArrayList) r.get("tags"); + found = found || tags.contains(API_TAG); + } + assertTrue(found); + } + + @Test + public void test07ResourceMetadata() throws Exception { + // should allow get resource metadata + Map resource = api.resource(API_TEST, ObjectUtils.emptyMap()); + assertNotNull(resource); + assertEquals(API_TEST, resource.get("public_id")); + assertEquals(3381, resource.get("bytes")); + assertEquals(1, ((List) resource.get("derived")).size()); + } + + @Test + public void test08DeleteDerived() throws Exception { + // should allow deleting derived resource + cloudinary.uploader().upload(SRC_TEST_IMAGE, + ObjectUtils.asMap("public_id", API_TEST_3, "tags", UPLOAD_TAGS, "eager", Collections.singletonList(new Transformation().width(101).crop("scale")))); + Map resource = api.resource(API_TEST_3, ObjectUtils.emptyMap()); + assertNotNull(resource); + List derived = (List) resource.get("derived"); + assertEquals(derived.size(), 1); + String derived_resource_id = (String) derived.get(0).get("id"); + api.deleteDerivedResources(Collections.singletonList(derived_resource_id), ObjectUtils.emptyMap()); + resource = api.resource(API_TEST_3, ObjectUtils.emptyMap()); + assertNotNull(resource); + derived = (List) resource.get("derived"); + assertEquals(derived.size(), 0); + } + + @Test() + public void testDeleteDerivedByTransformation() throws Exception { + // should allow deleting resources + String public_id = "api_test_123" + SUFFIX; + List transformations = new ArrayList(); + transformations.add(new Transformation().angle(90)); + transformations.add(new Transformation().width(120)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", public_id, "tags", UPLOAD_TAGS, "eager", transformations)); + Map resource = api.resource(public_id, ObjectUtils.emptyMap()); + assertNotNull(resource); + List derived = ((List) resource.get("derived")); + assertTrue(derived.size() == 2); + api.deleteDerivedByTransformation(ObjectUtils.asArray(public_id), ObjectUtils.asArray(transformations), ObjectUtils.emptyMap()); + + resource = api.resource(public_id, ObjectUtils.emptyMap()); + assertNotNull(resource); + derived = ((List) resource.get("derived")); + assertTrue(derived.size() == 0); + } + + @Test + public void testGetResourcesWithMetadata() throws Exception { + String public_id = "api_,withMetadata" + SUFFIX; + String fieldId = MetadataTestHelper.addFieldToAccount(api, MetadataTestHelper.newFieldInstance("some_field" + SUFFIX, true)).get("external_id").toString(); + cloudinary.uploader().upload(SRC_TEST_IMAGE, + ObjectUtils.asMap("public_id", public_id, + "tags", UPLOAD_TAGS, + "metadata", ObjectUtils.asMap(fieldId, "test"), + "moderation", "manual", + "context", ObjectUtils.asMap("name", "value"))); + + Map result = api.resources(ObjectUtils.asMap("metadata", false)); + assertNull(getMetadata(public_id, result)); + + result = api.resources(ObjectUtils.asMap("metadata", true)); + assertNotNull(getMetadata(public_id, result)); + + result = api.resourcesByTag(UPLOAD_TAGS[0], ObjectUtils.asMap("metadata", true)); + assertNotNull(getMetadata(public_id, result)); + + result = api.resourcesByTag(UPLOAD_TAGS[0], ObjectUtils.asMap("metadata", false)); + assertNull(getMetadata(public_id, result)); + + result = api.resourcesByModeration("manual", "pending", ObjectUtils.asMap("metadata", true)); + assertNotNull(getMetadata(public_id, result)); + + result = api.resourcesByModeration("manual", "pending", ObjectUtils.asMap("metadata", false)); + assertNull(getMetadata(public_id, result)); + + result = api.resourcesByContext("name", "value", ObjectUtils.asMap("metadata", true)); + assertNotNull(getMetadata(public_id, result)); + + result = api.resourcesByContext("name", "value", ObjectUtils.asMap("metadata", false)); + assertNull(getMetadata(public_id, result)); + } + + private Object getMetadata(String public_id, Map result) { + Map resource = findByAttr((List) result.get("resources"), "public_id", public_id); + return resource.get("metadata"); + } + + @Test(expected = NotFound.class) + public void test09DeleteResources() throws Exception { + // should allow deleting resources + String public_id = "api_,test3" + SUFFIX; + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", public_id, "tags", UPLOAD_TAGS)); + Map resource = api.resource(public_id, ObjectUtils.emptyMap()); + assertNotNull(resource); + api.deleteResources(Arrays.asList(public_id), ObjectUtils.emptyMap()); + api.resource(public_id, ObjectUtils.emptyMap()); + } + + @Test(expected = NotFound.class) + public void test10DeleteResourcesByAssetsIds() throws Exception { + String public_id = "api_,test4" + SUFFIX; + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", public_id, "tags", UPLOAD_TAGS)); + Map resource = api.resource(public_id, ObjectUtils.emptyMap()); + assertNotNull(resource); + String assetId = (String)resource.get("asset_id"); + ApiResponse response = api.deleteResourcesByAssetIds(Arrays.asList(assetId), ObjectUtils.emptyMap()); + assertNotNull(response); + assertNotNull(response.get("deleted")); + assertNotNull(response.get("deleted_counts")); + api.resource(public_id, ObjectUtils.emptyMap()); + } + + @Test(expected = NotFound.class) + public void test09aDeleteResourcesByPrefix() throws Exception { + // should allow deleting resources + String public_id = SUFFIX + "_api_test_by_prefix"; + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", public_id, "tags", UPLOAD_TAGS)); + Map resource = api.resource(public_id, ObjectUtils.emptyMap()); + assertNotNull(resource); + api.deleteResourcesByPrefix(public_id.substring(0, SUFFIX.length() + 10), ObjectUtils.emptyMap()); + api.resource(public_id, ObjectUtils.emptyMap()); + } + + @Test(expected = NotFound.class) + public void test09aDeleteResourcesByTags() throws Exception { + // should allow deleting resources + String tag = "api_test_tag_for_delete" + SUFFIX; + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", API_TEST + "_4", "tags", Collections.singletonList(tag))); + Map resource = api.resource(API_TEST + "_4", ObjectUtils.emptyMap()); + assertNotNull(resource); + api.deleteResourcesByTag(tag, ObjectUtils.emptyMap()); + api.resource(API_TEST + "_4", ObjectUtils.emptyMap()); + } + + @Test + public void test10Tags() throws Exception { + // should allow listing tags + Map result = api.tags(ObjectUtils.asMap("max_results", 10)); + List tags = (List) result.get("tags"); + assertNotNull(tags); + assertTrue(tags.size() > 0); + } + + @Test + public void test11TagsPrefix() throws Exception { + // should allow listing tag by prefix + Map result = api.tags(ObjectUtils.asMap("prefix", API_TAG.substring(0, API_TAG.length() - 1))); + List tags = (List) result.get("tags"); + assertThat(tags, hasItem(API_TAG)); + result = api.tags(ObjectUtils.asMap("prefix", "api_test_no_such_tag")); + tags = (List) result.get("tags"); + assertEquals(0, tags.size()); + } + + @Test + public void test12Transformations() throws Exception { + // should allow listing transformations + final Transformation listTest = new Transformation().width(25).crop("scale").overlay(new TextLayer().text(SUFFIX + "_testListTransformations").fontFamily("Arial").fontSize(60)); + preloadResource(ObjectUtils.asMap("tags", UPLOAD_TAGS, "eager", Collections.singletonList(listTest))); + Map result = api.transformations(ObjectUtils.asMap("max_results", 500)); + Map transformation = findByAttr((List) result.get("transformations"), "name", listTest.generate()); + + assertNotNull(transformation); + assertTrue((Boolean) transformation.get("used")); + } + + @Test + public void test13TransformationMetadata() throws Exception { + // should allow getting transformation metadata + preloadResource(ObjectUtils.asMap("tags", UPLOAD_TAGS, "eager", Collections.singletonList(EXPLICIT_TRANSFORMATION))); + Map transformation = api.transformation(EXPLICIT_TRANSFORMATION_NAME, ObjectUtils.asMap("max_results", 500)); + assertNotNull(transformation); + assertEquals(new Transformation((List) transformation.get("info")).generate(), EXPLICIT_TRANSFORMATION.generate()); + } + + @Test + public void test14TransformationUpdate() throws Exception { + // should allow updating transformation allowed_for_strict + api.updateTransformation(UPDATE_TRANSFORMATION_NAME, ObjectUtils.asMap("allowed_for_strict", true), ObjectUtils.emptyMap()); + Map transformation = api.transformation(UPDATE_TRANSFORMATION_NAME, ObjectUtils.emptyMap()); + assertNotNull(transformation); + assertEquals(transformation.get("allowed_for_strict"), true); + api.updateTransformation(UPDATE_TRANSFORMATION_NAME, ObjectUtils.asMap("allowed_for_strict", false), ObjectUtils.emptyMap()); + transformation = api.transformation(UPDATE_TRANSFORMATION_NAME, ObjectUtils.emptyMap()); + assertNotNull(transformation); + assertEquals(transformation.get("allowed_for_strict"), false); + } + + @Test + public void test15TransformationCreate() throws Exception { + // should allow creating named transformation + api.createTransformation(API_TEST_TRANSFORMATION, new Transformation().crop("scale").width(102).generate(), ObjectUtils.emptyMap()); + Map transformation = api.transformation(API_TEST_TRANSFORMATION, ObjectUtils.emptyMap()); + assertNotNull(transformation); + assertEquals(transformation.get("allowed_for_strict"), true); + assertEquals(new Transformation((List) transformation.get("info")).generate(), new Transformation().crop("scale").width(102).generate()); + assertEquals(transformation.get("used"), false); + } + + @Test + public void test15aTransformationUnsafeUpdate() throws Exception { + // should allow unsafe update of named transformation + api.createTransformation(API_TEST_TRANSFORMATION_3, new Transformation().crop("scale").width(102).generate(), ObjectUtils.emptyMap()); + api.updateTransformation(API_TEST_TRANSFORMATION_3, ObjectUtils.asMap("unsafe_update", new Transformation().crop("scale").width(103).generate()), + ObjectUtils.emptyMap()); + Map transformation = api.transformation(API_TEST_TRANSFORMATION_3, ObjectUtils.emptyMap()); + assertNotNull(transformation); + assertEquals(new Transformation((List) transformation.get("info")).generate(), new Transformation().crop("scale").width(103).generate()); + assertEquals(transformation.get("used"), false); + } + + @Test(expected = NotFound.class) + public void test16aTransformationDelete() throws Exception { + // should allow deleting named transformation + api.createTransformation(API_TEST_TRANSFORMATION_2, new Transformation().crop("scale").width(103).generate(), ObjectUtils.emptyMap()); + api.transformation(API_TEST_TRANSFORMATION_2, ObjectUtils.emptyMap()); + ApiResponse res = api.deleteTransformation(API_TEST_TRANSFORMATION_2, ObjectUtils.emptyMap()); + assertEquals("deleted", res.get("message")); + api.transformation(API_TEST_TRANSFORMATION_2, ObjectUtils.emptyMap()); + } + + @Test(expected = NotFound.class) + public void test17aTransformationDeleteImplicit() throws Exception { + // should allow deleting implicit transformation + api.transformation(DELETE_TRANSFORMATION_NAME, ObjectUtils.emptyMap()); + ApiResponse res = api.deleteTransformation(DELETE_TRANSFORMATION_NAME, ObjectUtils.emptyMap()); + assertEquals("deleted", res.get("message")); + api.deleteTransformation(DELETE_TRANSFORMATION_NAME, ObjectUtils.emptyMap()); + } + + @Test + public void testListTransformationByNamed() throws Exception { + String name = "a_test_named_transformation_param" + SUFFIX; + try { + api.createTransformation(name, "w_100", null); + name = "t_" + name; + List named = (List) api.transformations(ObjectUtils.asMap("max_results", 30, "named", true)).get("transformations"); + List unnamed = (List) api.transformations(ObjectUtils.asMap("max_results", 30, "named", false)).get("transformations"); + + // the named transformation should be present only in the named list: + boolean unnamedFound = false; + boolean namedFound = false; + + for (Map t : unnamed) { + if (t.get("name").equals(name)) { + unnamedFound = true; + break; + } + } + + if (!unnamedFound) { + for (Map t : named) { + if (t.get("name").equals(name)) { + namedFound = true; + break; + } + } + } + + assertTrue("Named transformation wasn't returned with named=true param", namedFound); + assertFalse("Named transformation returned with named=false param", unnamedFound); + + } finally { + try { + api.deleteTransformation(name, null); + } catch (Exception ignored) { + } + } + } + + @Test + public void test20ResourcesContext() throws Exception { + Map result = api.resourcesByContext(TEST_KEY, ObjectUtils.emptyMap()); + + List resources = (List) result.get("resources"); + assertEquals(2, resources.size()); + result = api.resourcesByContext(TEST_KEY, "alt", ObjectUtils.emptyMap()); + + resources = (List) result.get("resources"); + assertEquals(1, resources.size()); + } + + @Test + public void test18Usage() throws Exception { + // should support usage API call + final Date yesterday = yesterday(); + + Map result = api.usage(ObjectUtils.asMap("date", yesterday)); + assertNotNull(result.get("last_updated")); + + result = api.usage(ObjectUtils.asMap("date", ObjectUtils.toUsageApiDateFormat(yesterday))); + assertNotNull(result.get("last_updated")); + + result = api.usage(ObjectUtils.emptyMap()); + assertNotNull(result.get("last_updated")); + } + + private Date yesterday() { + return new Date(new Date().getTime() - 24 * 60 * 60 * 1000); + } + + @Test + public void testRateLimitWithNonEnglishLocale() throws Exception { + Locale.setDefault(new Locale("de", "DE")); + ApiResponse result = cloudinary.api().usage(new HashMap()); + Assert.assertNotNull(result.apiRateLimit().getReset()); + } + + @Test + public void testRateLimits() throws Exception { + ApiResponse result = cloudinary.api().usage(new HashMap()); + Assert.assertNotEquals(0, result.apiRateLimit().getLimit()); + Assert.assertNotNull(result.apiRateLimit().getReset()); + Assert.assertNotEquals(0, result.apiRateLimit().getRemaining()); + } + + @Test + public void testConfiguration() throws Exception { + ApiResponse result = cloudinary.api().configuration(ObjectUtils.asMap("settings", true)); + Map settings = (Map) result.get("settings"); + Assert.assertNotNull(settings.get("folder_mode")); + } + + @Test + public void test19Ping() throws Exception { + // should support ping API call + Map result = api.ping(ObjectUtils.emptyMap()); + assertEquals(result.get("status"), "ok"); + } + + // This test must be last because it deletes (potentially) all dependent + // transformations which some tests rely on. + // Add @Test if you really want to test it - This test deletes derived + // resources! + public void testDeleteAllResources() throws Exception { + // should allow deleting all resources + cloudinary.uploader().upload(SRC_TEST_IMAGE, + ObjectUtils.asMap("public_id", API_TEST_5, "tags", UPLOAD_TAGS, "eager", Collections.singletonList(new Transformation().crop("scale").width(2.0)))); + Map result = api.resource(API_TEST_5, ObjectUtils.emptyMap()); + assertEquals(1, ((org.cloudinary.json.JSONArray) result.get("derived")).length()); + api.deleteAllResources(ObjectUtils.asMap("keep_original", true)); + result = api.resource(API_TEST_5, ObjectUtils.emptyMap()); + // assertEquals(0, ((org.cloudinary.json.JSONArray) + // result.get("derived")).size()); + } + + @Test + public void testManualModeration() throws Exception { + // should support setting manual moderation status + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("moderation", "manual", "tags", UPLOAD_TAGS)); + Map apiResult = api.update((String) uploadResult.get("public_id"), ObjectUtils.asMap("moderation_status", "approved", "tags", UPLOAD_TAGS)); + assertEquals("approved", ((Map) ((List) apiResult.get("moderation")).get(0)).get("status")); + } + + @Test + public void testOcrUpdate() throws Exception { + assumeAddonEnabled("ocr"); + Exception expected = null; + // should support requesting ocr info + try { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + api.update((String) uploadResult.get("public_id"), ObjectUtils.asMap("ocr", "illegal")); + } catch (Exception e) { + expected = e; + } + + assertNotNull(expected); + assertTrue(expected instanceof BadRequest); + assertTrue(expected.getMessage().matches("^Illegal value(.*)")); + } + + @Test + public void testRawConvertUpdate() { + // should support requesting raw conversion + try { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + api.update((String) uploadResult.get("public_id"), ObjectUtils.asMap("raw_convert", "illegal")); + } catch (Exception e) { + assertTrue(e instanceof BadRequest); + assertTrue(e.getMessage().matches("^Illegal value(.*)")); + } + } + + @Test + public void testCategorizationUpdate() { + // should support requesting categorization + try { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + api.update((String) uploadResult.get("public_id"), ObjectUtils.asMap("categorization", "illegal")); + } catch (Exception e) { + assertTrue(e instanceof BadRequest); + assertTrue(e.getMessage().matches("^Illegal value(.*)")); + } + } + + @Test + public void testDetectionUpdate() { + // should support requesting detection + try { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + api.update((String) uploadResult.get("public_id"), ObjectUtils.asMap("detection", "illegal")); + } catch (Exception e) { + assertTrue(e instanceof BadRequest); + assertTrue(e.getMessage().matches("^Illegal value(.*)")); + } + } + + @Test + public void testUpdateResourceClearInvalid() throws Exception { + String fieldId = MetadataTestHelper.addFieldToAccount(api, MetadataTestHelper.newFieldInstance("some_field3" + SUFFIX, true)).get("external_id").toString(); + String fieldId2 = MetadataTestHelper.addFieldToAccount(api, MetadataTestHelper.newFieldInstance("some_field4" + SUFFIX, true)).get("external_id").toString(); + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, + ObjectUtils.asMap("tags", UPLOAD_TAGS, "metadata", ObjectUtils.asMap(fieldId, "test"))); + Map apiResult = api.update((String) uploadResult.get("public_id"), ObjectUtils.asMap("clear_invalid", true, "metadata", ObjectUtils.asMap(fieldId2, "test2"))); + assertNotNull(((Map)apiResult.get("metadata")).get(fieldId2)); + } + + @Test + public void testUpdateCustomCoordinates() throws IOException, Exception { + // should update custom coordinates + Coordinates coordinates = new Coordinates("121,31,110,151"); + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + cloudinary.api().update(uploadResult.get("public_id").toString(), ObjectUtils.asMap("custom_coordinates", coordinates)); + Map result = cloudinary.api().resource(uploadResult.get("public_id").toString(), ObjectUtils.asMap("coordinates", true)); + int[] expected = new int[]{121, 31, 110, 151}; + ArrayList actual = (ArrayList) ((ArrayList) ((Map) result.get("coordinates")).get("custom")).get(0); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actual.get(i)); + } + } + + @Test + public void testUpdateAccessControl() throws Exception { + // should update access control + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + final Date start = simpleDateFormat.parse("2019-02-22 16:20:57 +0200"); + final Date end = simpleDateFormat.parse("2019-03-22 00:00:00 +0200"); + AccessControlRule acl = AccessControlRule.anonymous(start, end); + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS)); + ApiResponse res = cloudinary.api().update(uploadResult.get("public_id").toString(), ObjectUtils.asMap("access_control", acl)); + Map result = cloudinary.api().resource(uploadResult.get("public_id").toString(), ObjectUtils.asMap("access_control", true)); + + Map accessControlResult = (Map) ((List) result.get("access_control")).get(0); + + assertEquals("anonymous", accessControlResult.get("access_type")); + assertEquals("2019-02-22T14:20:57Z", accessControlResult.get("start")); + assertEquals("2019-03-21T22:00:00Z", accessControlResult.get("end")); + } + + @Test + public void testListUploadPresets() throws Exception { + // should allow creating and listing upload_presets + api.createUploadPreset(ObjectUtils.asMap("name", API_TEST_UPLOAD_PRESET, "folder", "folder")); + api.createUploadPreset(ObjectUtils.asMap("name", API_TEST_UPLOAD_PRESET_2, "folder", "folder2")); + api.createUploadPreset(ObjectUtils.asMap("name", API_TEST_UPLOAD_PRESET_3, "folder", "folder3")); + + ArrayList presets = (ArrayList) (api.uploadPresets(ObjectUtils.emptyMap()).get("presets")); + + assertThat(presets, hasItem(hasEntry("name", API_TEST_UPLOAD_PRESET))); + assertThat(presets, hasItem(hasEntry("name", API_TEST_UPLOAD_PRESET_2))); + assertThat(presets, hasItem(hasEntry("name", API_TEST_UPLOAD_PRESET_3))); + + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET, ObjectUtils.emptyMap()); + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET_2, ObjectUtils.emptyMap()); + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET_3, ObjectUtils.emptyMap()); + } + + @Test + public void testGetUploadPreset() throws Exception { + // should allow getting a single upload_preset + String[] tags = {"a", "b", "c"}; + Map context = ObjectUtils.asMap("a", "b", "c", "d"); + Map result = api.createUploadPreset(ObjectUtils.asMap("unsigned", true, "folder", "folder", "transformation", EXPLICIT_TRANSFORMATION, "tags", tags, "context", + context, "use_asset_folder_as_public_id_prefix", true)); + String name = result.get("name").toString(); + Map preset = api.uploadPreset(name, ObjectUtils.emptyMap()); + assertEquals(preset.get("name"), name); + assertEquals(Boolean.TRUE, preset.get("unsigned")); + Map settings = (Map) preset.get("settings"); + assertEquals(settings.get("folder"), "folder"); + assertEquals(settings.get("use_asset_folder_as_public_id_prefix"), true); + Map outTransformation = (Map) ((java.util.ArrayList) settings.get("transformation")).get(0); + assertEquals(outTransformation.get("width"), 100); + assertEquals(outTransformation.get("crop"), "scale"); + Object[] outTags = ((java.util.ArrayList) settings.get("tags")).toArray(); + assertArrayEquals(tags, outTags); + Map outContext = (Map) settings.get("context"); + assertEquals(context, outContext); + + api.deleteUploadPreset(name, ObjectUtils.emptyMap()); + } + + @Test + public void testDeleteUploadPreset() throws Exception { + // should allow deleting upload_presets", :upload_preset => true do + api.createUploadPreset(ObjectUtils.asMap("name", API_TEST_UPLOAD_PRESET_4, "folder", "folder")); + api.uploadPreset(API_TEST_UPLOAD_PRESET_4, ObjectUtils.emptyMap()); + api.deleteUploadPreset(API_TEST_UPLOAD_PRESET_4, ObjectUtils.emptyMap()); + boolean error = false; + try { + api.uploadPreset(API_TEST_UPLOAD_PRESET_4, ObjectUtils.emptyMap()); + } catch (Exception e) { + error = true; + } + assertTrue(error); + } + + @Test + public void testUpdateUploadPreset() throws Exception { + // should allow updating upload_presets + String name = api.createUploadPreset(ObjectUtils.asMap("folder", "folder")).get("name").toString(); + Map preset = api.uploadPreset(name, ObjectUtils.emptyMap()); + Map settings = (Map) preset.get("settings"); + settings.putAll(ObjectUtils.asMap("colors", true, "unsigned", true, "disallow_public_id", true, "eval",AbstractUploaderTest.SRC_TEST_EVAL)); + api.updateUploadPreset(name, settings); + settings.remove("unsigned"); + preset = api.uploadPreset(name, ObjectUtils.emptyMap()); + assertEquals(name, preset.get("name")); + assertEquals(Boolean.TRUE, preset.get("unsigned")); + assertEquals(settings, preset.get("settings")); + + api.deleteUploadPreset(name, ObjectUtils.emptyMap()); + } + + @Test + public void testListByModerationUpdate() throws Exception { + // "should support listing by moderation kind and value + List resources; + + Map result1 = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("moderation", "manual", "tags", UPLOAD_TAGS)); + Map result2 = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("moderation", "manual", "tags", UPLOAD_TAGS)); + Map result3 = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("moderation", "manual", "tags", UPLOAD_TAGS)); + api.update((String) result1.get("public_id"), ObjectUtils.asMap("moderation_status", "approved")); + api.update((String) result2.get("public_id"), ObjectUtils.asMap("moderation_status", "rejected")); + Map approved = api.resourcesByModeration("manual", "approved", ObjectUtils.asMap("max_results", 1000)); + Map rejected = api.resourcesByModeration("manual", "rejected", ObjectUtils.asMap("max_results", 1000)); + Map pending = api.resourcesByModeration("manual", "pending", ObjectUtils.asMap("max_results", 1000)); + + resources = (List) approved.get("resources"); + assertThat(resources, hasItem(hasEntry("public_id", result1.get("public_id")))); + assertThat(resources, not(hasItem(hasEntry("public_id", result2.get("public_id"))))); + assertThat(resources, not(hasItem(hasEntry("public_id", result3.get("public_id"))))); + + resources = (List) rejected.get("resources"); + assertThat(resources, not(hasItem(hasEntry("public_id", result1.get("public_id"))))); + assertThat(resources, hasItem(hasEntry("public_id", result2.get("public_id")))); + assertThat(resources, not(hasItem(hasEntry("public_id", result3.get("public_id"))))); + + resources = (List) pending.get("resources"); + assertThat(resources, not(hasItem(hasEntry("public_id", result1.get("public_id"))))); + assertThat(resources, not(hasItem(hasEntry("public_id", result2.get("public_id"))))); + assertThat(resources, hasItem(hasEntry("public_id", result3.get("public_id")))); + } + + // For this test to work, "Auto-create folders" should be enabled in the + // Upload Settings. + // Uncomment @Test if you really want to test it. + // @Test + public void testFolderApi() throws Exception { + // should allow deleting all resources + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", "test_folder1/item", "tags", UPLOAD_TAGS)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", "test_folder2/item", "tags", UPLOAD_TAGS)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", "test_folder1/test_subfolder1/item", "tags", UPLOAD_TAGS)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("public_id", "test_folder1/test_subfolder2/item", "tags", UPLOAD_TAGS)); + Map result = api.rootFolders(null); + assertEquals("test_folder1", ((Map) ((org.cloudinary.json.JSONArray) result.get("folders")).get(0)).get("name")); + assertEquals("test_folder2", ((Map) ((org.cloudinary.json.JSONArray) result.get("folders")).get(1)).get("name")); + result = api.subFolders("test_folder1", null); + assertEquals("test_folder1/test_subfolder1", ((Map) ((org.cloudinary.json.JSONArray) result.get("folders")).get(0)).get("path")); + assertEquals("test_folder1/test_subfolder2", ((Map) ((org.cloudinary.json.JSONArray) result.get("folders")).get(1)).get("path")); + try { + api.subFolders("test_folder", null); + } catch (Exception e) { + assertTrue(e instanceof NotFound); + } + api.deleteResourcesByPrefix("test_folder", ObjectUtils.emptyMap()); + } + + @Test + public void testCreateFolder() throws Exception { + String apTestCreateFolder = "api_test_create_folder" + "_" + SUFFIX; + createdFolders.add(apTestCreateFolder); + Map result = api.createFolder("apTestCreateFolder", null); + assertTrue((Boolean) result.get("success")); + } + + @Test + public void testRestore() throws Exception { + // should support restoring resources + cloudinary.uploader().upload(SRC_TEST_IMAGE, + ObjectUtils.asMap("public_id", API_TEST_RESTORE, "backup", true, "tags", UPLOAD_TAGS)); + Map resource = api.resource(API_TEST_RESTORE, ObjectUtils.emptyMap()); + assertEquals(resource.get("bytes"), 3381); + api.deleteResources(Collections.singletonList(API_TEST_RESTORE), ObjectUtils.emptyMap()); + resource = api.resource(API_TEST_RESTORE, ObjectUtils.emptyMap()); + assertEquals(resource.get("bytes"), 0); + assertTrue((Boolean) resource.get("placeholder")); + Map response = api.restore(Collections.singletonList(API_TEST_RESTORE), ObjectUtils.emptyMap()); + Map info = (Map) response.get(API_TEST_RESTORE); + assertNotNull(info); + assertEquals(info.get("bytes"), 3381); + resource = api.resource(API_TEST_RESTORE, ObjectUtils.emptyMap()); + assertEquals(resource.get("bytes"), 3381); + } + + @Test + public void testRestoreByAssetIds() throws Exception { + + // Upload + cloudinary.uploader().upload(SRC_TEST_IMAGE, + ObjectUtils.asMap("public_id", API_TEST_RESTORE, "backup", true, "tags", UPLOAD_TAGS)); + Map resource = api.resource(API_TEST_RESTORE, ObjectUtils.emptyMap()); + assertEquals(resource.get("bytes"), 3381); + + //Delete + api.deleteResources(Collections.singletonList(API_TEST_RESTORE), ObjectUtils.emptyMap()); + resource = api.resource(API_TEST_RESTORE, ObjectUtils.emptyMap()); + String assetId = (String) resource.get("asset_id"); + assertEquals(resource.get("bytes"), 0); + assertNotNull(assetId); + assertTrue((Boolean) resource.get("placeholder")); + + //Restore + Map response = api.restoreByAssetIds(Collections.singletonList(assetId), ObjectUtils.emptyMap()); + Map info = (Map) response.get(assetId); + assertNotNull(info); + assertEquals(info.get("bytes"), 3381); + resource = api.resource(API_TEST_RESTORE, ObjectUtils.emptyMap()); + assertEquals(resource.get("bytes"), 3381); + } + + @Test + public void testRestoreDifferentVersionsOfDeletedAsset() throws Exception { + final String TEST_RESOURCE_PUBLIC_ID = "api_test_restore_different_versions_single_asset" + SUFFIX; + final Uploader uploader = cloudinary.uploader(); + + Map firstUpload = uploader.upload(SRC_TEST_IMAGE, + ObjectUtils.asMap( + "public_id", TEST_RESOURCE_PUBLIC_ID, + "backup", true, + "tags", UPLOAD_TAGS + )); + assertEquals(firstUpload.get("public_id"), TEST_RESOURCE_PUBLIC_ID); + Thread.sleep(SLEEP_TIMEOUT); + ApiResponse firstDelete = api.deleteResources(Collections.singletonList(TEST_RESOURCE_PUBLIC_ID), ObjectUtils.emptyMap()); + assertTrue(firstDelete.containsKey("deleted")); + Thread.sleep(SLEEP_TIMEOUT); + + Map secondUpload = uploader.upload(SRC_TEST_IMAGE, + ObjectUtils.asMap( + "public_id", TEST_RESOURCE_PUBLIC_ID, + "backup", true, + "transformation", new Transformation().angle("0"), + "tags", UPLOAD_TAGS + )); + assertEquals(secondUpload.get("public_id"), TEST_RESOURCE_PUBLIC_ID); + Thread.sleep(SLEEP_TIMEOUT); + ApiResponse secondDelete = api.deleteResources(Collections.singletonList(TEST_RESOURCE_PUBLIC_ID), ObjectUtils.emptyMap()); + assertTrue(secondDelete.containsKey("deleted")); + Thread.sleep(SLEEP_TIMEOUT); + assertNotEquals(firstUpload.get("bytes"), secondUpload.get("bytes")); + + ApiResponse getVersionsResp = api.resource(TEST_RESOURCE_PUBLIC_ID, ObjectUtils.asMap("versions", true)); + List versions = (List) getVersionsResp.get("versions"); + Assert.assertTrue(versions.size() > 1); + Object firstAssetVersion = versions.get(0).get("version_id"); + Object secondAssetVersion = versions.get(1).get("version_id"); + + ApiResponse firstVerRestore = api.restore(Collections.singletonList(TEST_RESOURCE_PUBLIC_ID), + ObjectUtils.asMap("versions", Collections.singletonList(firstAssetVersion))); + assertEquals(((Map) firstVerRestore.get(TEST_RESOURCE_PUBLIC_ID)).get("bytes"), firstUpload.get("bytes")); + + ApiResponse secondVerRestore = api.restore(Collections.singletonList(TEST_RESOURCE_PUBLIC_ID), + ObjectUtils.asMap("versions", Collections.singletonList(secondAssetVersion))); + assertEquals(((Map) secondVerRestore.get(TEST_RESOURCE_PUBLIC_ID)).get("bytes"), secondUpload.get("bytes")); + Thread.sleep(SLEEP_TIMEOUT); + ApiResponse finalDeleteResp = api.deleteResources(Collections.singletonList(TEST_RESOURCE_PUBLIC_ID), ObjectUtils.emptyMap()); + assertTrue(finalDeleteResp.containsKey("deleted")); + } + + @Test + public void testShouldRestoreTwoDifferentDeletedAssets() throws Exception { + final String PUBLIC_ID_BACKUP_1 = "api_test_restore_versions_different_assets_1_" + SUFFIX; + final String PUBLIC_ID_BACKUP_2 = "api_test_restore_versions_different_assets_2_" + SUFFIX; + + final Uploader uploader = cloudinary.uploader(); + + Map firstUpload = uploader.upload(SRC_TEST_IMAGE, + ObjectUtils.asMap( + "public_id", PUBLIC_ID_BACKUP_1, + "backup", true, + "tags", UPLOAD_TAGS + )); + Map secondUpload = uploader.upload(SRC_TEST_IMAGE, + ObjectUtils.asMap( + "public_id", PUBLIC_ID_BACKUP_2, + "backup", true, + "transformation", new Transformation().angle("0"), + "tags", UPLOAD_TAGS + )); + + ApiResponse deleteAll = api.deleteResources(Arrays.asList(PUBLIC_ID_BACKUP_1, PUBLIC_ID_BACKUP_2), ObjectUtils.emptyMap()); + assertEquals("deleted", ((Map) deleteAll.get("deleted")).get(PUBLIC_ID_BACKUP_1)); + assertEquals("deleted", ((Map) deleteAll.get("deleted")).get(PUBLIC_ID_BACKUP_2)); + + ApiResponse getFirstAssetVersion = api.resource(PUBLIC_ID_BACKUP_1, ObjectUtils.asMap("versions", true)); + ApiResponse getSecondAssetVersion = api.resource(PUBLIC_ID_BACKUP_2, ObjectUtils.asMap("versions", true)); + + Object firstAssetVersion = ((List) getFirstAssetVersion.get("versions")).get(0).get("version_id"); + Object secondAssetVersion = ((List) getSecondAssetVersion.get("versions")).get(0).get("version_id"); + + ApiResponse restore = api.restore(Arrays.asList(PUBLIC_ID_BACKUP_1, PUBLIC_ID_BACKUP_2), + ObjectUtils.asMap("versions", Arrays.asList(firstAssetVersion, secondAssetVersion))); + assertEquals(((Map) restore.get(PUBLIC_ID_BACKUP_1)).get("bytes"), firstUpload.get("bytes")); + assertEquals(((Map) restore.get(PUBLIC_ID_BACKUP_2)).get("bytes"), secondUpload.get("bytes")); + + ApiResponse finalDelete = api.deleteResources(Arrays.asList(PUBLIC_ID_BACKUP_1, PUBLIC_ID_BACKUP_2), ObjectUtils.emptyMap()); + assertEquals("deleted", ((Map) finalDelete.get("deleted")).get(PUBLIC_ID_BACKUP_1)); + assertEquals("deleted", ((Map) finalDelete.get("deleted")).get(PUBLIC_ID_BACKUP_2)); + } + + @Test + public void testEncodeUrlInApiCall() throws Exception { + String apiTestEncodeUrlInApiCall = "sub^folder test"; + createdFolders.add(apiTestEncodeUrlInApiCall); + Map result = api.createFolder(apiTestEncodeUrlInApiCall, null); + assertEquals("sub^folder test", result.get("path")); + } + + @Test + public void testUploadMapping() throws Exception { + String aptTestUploadMapping = "api_test_upload_mapping" + SUFFIX; + try { + api.deleteUploadMapping(aptTestUploadMapping, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + + } + api.createUploadMapping(aptTestUploadMapping, ObjectUtils.asMap("template", "http://cloudinary.com")); + Map result = api.uploadMapping(aptTestUploadMapping, ObjectUtils.emptyMap()); + assertEquals(result.get("template"), "http://cloudinary.com"); + api.updateUploadMapping(aptTestUploadMapping, ObjectUtils.asMap("template", "http://res.cloudinary.com")); + result = api.uploadMapping(aptTestUploadMapping, ObjectUtils.emptyMap()); + assertEquals(result.get("template"), "http://res.cloudinary.com"); + result = api.uploadMappings(ObjectUtils.emptyMap()); + ListIterator mappings = ((ArrayList) result.get("mappings")).listIterator(); + boolean found = false; + while (mappings.hasNext()) { + Map mapping = (Map) mappings.next(); + if (mapping.get("folder").equals(aptTestUploadMapping) + && mapping.get("template").equals("http://res.cloudinary.com")) { + found = true; + break; + } + } + assertTrue(found); + api.deleteUploadMapping(aptTestUploadMapping, ObjectUtils.emptyMap()); + result = api.uploadMappings(ObjectUtils.emptyMap()); + found = false; + while (mappings.hasNext()) { + Map mapping = (Map) mappings.next(); + if (mapping.get("folder").equals(aptTestUploadMapping) + && mapping.get("template").equals("http://res.cloudinary.com")) { + found = true; + break; + } + } + assertTrue(!found); + } + + @Test + public void testPublishByIds() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS, "type", "authenticated")); + String publicId = (String) response.get("public_id"); + response = cloudinary.api().publishByIds(Arrays.asList(publicId), null); + List published = (List) response.get("published"); + assertNotNull(published); + assertEquals(published.size(), 1); + Map resource = (Map) published.get(0); + assertEquals(resource.get("public_id"), publicId); + assertNotNull(resource.get("url")); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testPublishWithType() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS, "type", "authenticated")); + String publicId = (String) response.get("public_id"); + + // publish with wrong type - verify publish fails + response = cloudinary.api().publishByIds(Arrays.asList(publicId), ObjectUtils.asMap("type", "private")); + List published = (List) response.get("published"); + List failed = (List) response.get("failed"); + assertNotNull(published); + assertNotNull(failed); + assertEquals(published.size(), 0); + assertEquals(failed.size(), 1); + + // publish with correct type - verify publish succeeds + response = cloudinary.api().publishByIds(Arrays.asList(publicId), ObjectUtils.asMap("type", "authenticated")); + published = (List) response.get("published"); + failed = (List) response.get("failed"); + assertNotNull(published); + assertNotNull(failed); + assertEquals(published.size(), 1); + assertEquals(failed.size(), 0); + + Map resource = (Map) published.get(0); + assertEquals(resource.get("public_id"), publicId); + assertNotNull(resource.get("url")); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testPublishByPrefix() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS, "type", "authenticated")); + String publicId = (String) response.get("public_id"); + response = cloudinary.api().publishByPrefix(publicId.substring(0, publicId.length() - 2), null); + List published = (List) response.get("published"); + assertNotNull(published); + assertEquals(published.size(), 1); + Map resource = (Map) published.get(0); + assertEquals(resource.get("public_id"), publicId); + assertNotNull(resource.get("url")); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testPublishByTag() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", Arrays.asList(API_TAG, API_TAG + "1"), "type", "authenticated")); + String publicId = (String) response.get("public_id"); + response = cloudinary.api().publishByTag(API_TAG + "1", null); + List published = (List) response.get("published"); + assertNotNull(published); + assertEquals(published.size(), 1); + Map resource = (Map) published.get(0); + assertEquals(resource.get("public_id"), publicId); + assertNotNull(resource.get("url")); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testUpdateResourcesAccessModeByIds() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS, "access_mode", "authenticated")); + String publicId = (String) response.get("public_id"); + assertEquals(response.get("access_mode"), "authenticated"); + response = cloudinary.api().updateResourcesAccessModeByIds("public", Arrays.asList(publicId), null); + List updated = (List) response.get("updated"); + assertNotNull(updated); + assertEquals(updated.size(), 1); + Map resource = (Map) updated.get(0); + assertEquals(resource.get("public_id"), publicId); + assertEquals(resource.get("access_mode"), "public"); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testUpdateResourcesAccessModeByPrefix() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", UPLOAD_TAGS, "access_mode", "authenticated")); + String publicId = (String) response.get("public_id"); + assertEquals(response.get("access_mode"), "authenticated"); + response = cloudinary.api().updateResourcesAccessModeByPrefix("public", publicId.substring(0, publicId.length() - 2), null); + List updated = (List) response.get("updated"); + assertNotNull(updated); + assertEquals(updated.size(), 1); + Map resource = (Map) updated.get(0); + assertEquals(resource.get("public_id"), publicId); + assertEquals(resource.get("access_mode"), "public"); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testUpdateResourcesAccessModeByTag() throws Exception { + Map response = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("tags", Arrays.asList(API_TAG, API_TAG + "2"), "access_mode", "authenticated")); + String publicId = (String) response.get("public_id"); + assertEquals(response.get("access_mode"), "authenticated"); + response = cloudinary.api().updateResourcesAccessModeByTag("public", API_TAG + "2", null); + List updated = (List) response.get("updated"); + assertNotNull(updated); + assertEquals(updated.size(), 1); + Map resource = (Map) updated.get(0); + assertEquals(resource.get("public_id"), publicId); + assertEquals(resource.get("access_mode"), "public"); + cloudinary.uploader().destroy(publicId, null); + } + + @Test + public void testQualityAnalysis() throws Exception { + ApiResponse result = cloudinary.api().resource(API_TEST, ObjectUtils.asMap("quality_analysis", true)); + assertNotNull(result.get("quality_analysis")); + } + + @Test(expected = NotFound.class) + public void testDeleteFolder() throws Exception { + String toDelete = "todelete_" + SUFFIX; + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", UPLOAD_TAGS, "folder", toDelete)); + Thread.sleep(SLEEP_TIMEOUT); + api.deleteResources(Collections.singletonList(uploadResult.get("public_id").toString()), emptyMap()); + ApiResponse result = api.deleteFolder(toDelete, emptyMap()); + assertTrue(((ArrayList) result.get("deleted")).contains(toDelete)); + + // should throw exception (folder not found): + api.deleteFolder(cloudinary.randomPublicId(), emptyMap()); + } + + + @Test + public void testCinemagraphAnalysisResource() throws Exception { + ApiResponse res = api.resource(API_TEST, Collections.singletonMap("cinemagraph_analysis", true)); + assertNotNull(res.get("cinemagraph_analysis")); + } + + @Test + public void testAccessibilityAnalysisResource() throws Exception { + ApiResponse res = api.resource(API_TEST, Collections.singletonMap("accessibility_analysis", true)); + assertNotNull(res.get("accessibility_analysis")); + } + + @Test + public void testAnalyzeApi() throws Exception { + assumeAddonEnabled("captioning"); + ApiResponse res = api.analyze("uri", "captioning", "https://res.cloudinary.com/demo/image/upload/dog", ObjectUtils.emptyMap()); + assertNotNull(res); + assertNotNull(res.get("request_id")); + } + + @Test + public void testFolderDecoupling() { + //TODO: Need to build a unit testing infrastructure + Map params = new HashMap(); + Map options = asMap( + "asset_folder", "new_asset_folder", + "unique_display_name", true); + Util.processWriteParameters(options, params); + assertEquals("new_asset_folder", params.get("asset_folder")); + assertEquals(true, params.get("unique_display_name")); + } + + @Test + public void testVisualSearch() { + //TODO: Need to build a unit testing infrastructure + Map params = new HashMap(); + Map options = asMap( + "visual_search", true); + Util.processWriteParameters(options, params); + assertEquals(true, params.get("visual_search")); + } + + @Test + @Ignore("Skip test till FD is enabled for test accounts") + public void testRenameFolder() throws Exception { + Map result = api.createFolder("apiTestCreateFolder" + SUFFIX, null); + assertNotNull(result); + + String folderName = (String) result.get("path"); + Map response = api.renameFolder(folderName, "newFolderName" + SUFFIX, ObjectUtils.emptyMap()); + assertNotNull(response); + } + + @Test + public void testDeleteBackedupAsset() throws Exception { + if (MockableTest.shouldTestFeature(Feature.BACKEDUP_ASSETS)) { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.asMap("backup", true)); + + String publicId = (String) result.get("public_id"); + String assetId = (String) result.get("asset_id"); + + ApiResponse getVersionsResp = api.resource(publicId, ObjectUtils.asMap("versions", true)); + List versions = (List) getVersionsResp.get("versions"); + String firstAssetVersion = (String) versions.get(0).get("version_id"); + ApiResponse response = api.deleteBackedUpAssets(assetId, new String[]{firstAssetVersion}, ObjectUtils.emptyMap()); + + assertNotNull(response); + assertEquals(response.get("asset_id"), assetId); + List deletedVersionIds = (List) response.get("deleted_version_ids"); + assertEquals(deletedVersionIds.get(0), firstAssetVersion); + } + } + + @Test + public void testAllowDerivedNextCursor() throws Exception { + String publicId = "allowderivednextcursor_" + SUFFIX; + Map options = ObjectUtils.asMap("public_id", publicId, "eager", Arrays.asList( + new Transformation().width(100), + new Transformation().width(101), + new Transformation().width(102) + )); + + try { + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + ApiResponse res = api.resource(publicId, Collections.singletonMap("max_results", 1)); + String derivedNextCursor = res.get("derived_next_cursor").toString(); + assertNotNull(derivedNextCursor); + + ApiResponse res2 = api.resource(publicId, ObjectUtils.asMap("derived_next_cursor", derivedNextCursor, "max_results", 1)); + String derivedNextCursor2 = res2.get("derived_next_cursor").toString(); + assertNotNull(derivedNextCursor2); + + assertNotEquals(derivedNextCursor, derivedNextCursor2); + } finally { + cloudinary.uploader().destroy(publicId, Collections.singletonMap("invalidate", true)); + } + } + + @Test + public void testSignatureWithEscapingCharacters() { + String API_SIGN_REQUEST_CLOUD_NAME = "dn6ot3ged"; + String API_SIGN_REQUEST_TEST_SECRET = "hdcixPpR2iKERPwqvH6sHdK9cyac"; + + Map paramsWithAmpersand = new HashMap<>(); + paramsWithAmpersand.put("cloud_name", API_SIGN_REQUEST_CLOUD_NAME); + paramsWithAmpersand.put("timestamp", 1568810420); + paramsWithAmpersand.put("notification_url", "https://fake.com/callback?a=1&tags=hello,world"); + + String signatureWithAmpersand = Util.produceSignature(paramsWithAmpersand, API_SIGN_REQUEST_TEST_SECRET, cloudinary.config.signatureVersion); + + Map paramsSmuggled = new HashMap<>(); + paramsSmuggled.put("cloud_name", API_SIGN_REQUEST_CLOUD_NAME); + paramsSmuggled.put("timestamp", 1568810420); + paramsSmuggled.put("notification_url", "https://fake.com/callback?a=1"); + paramsSmuggled.put("tags", "hello,world"); + + String signatureSmuggled = Util.produceSignature(paramsSmuggled, API_SIGN_REQUEST_TEST_SECRET, cloudinary.config.signatureVersion); + + assertNotEquals(signatureWithAmpersand, signatureSmuggled, + "Signatures should be different to prevent parameter smuggling"); + + String expectedSignature = "4fdf465dd89451cc1ed8ec5b3e314e8a51695704"; + assertEquals(expectedSignature, signatureWithAmpersand); + + String expectedSmuggledSignature = "7b4e3a539ff1fa6e6700c41b3a2ee77586a025f9"; + assertEquals(expectedSmuggledSignature, signatureSmuggled); + + String versionOneSignature = Util.produceSignature(paramsSmuggled, API_SIGN_REQUEST_TEST_SECRET, 1); + + assertEquals(expectedSmuggledSignature, versionOneSignature); + + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractContextTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractContextTest.java new file mode 100644 index 00000000..e785b5c0 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractContextTest.java @@ -0,0 +1,100 @@ +package com.cloudinary.test; + +import com.cloudinary.Cloudinary; +import com.cloudinary.Transformation; +import com.cloudinary.Uploader; +import com.cloudinary.utils.ObjectUtils; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static com.cloudinary.utils.ObjectUtils.asMap; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; +import static org.junit.Assume.assumeNotNull; + +@SuppressWarnings({"rawtypes", "unchecked"}) +abstract public class AbstractContextTest extends MockableTest { + + private static final String CONTEXT_TAG = "context_tag_" + String.valueOf(System.currentTimeMillis()) + SUFFIX; + public static final Map CONTEXT = asMap("caption", "some cäption", "alt", "alternativè"); + private Uploader uploader; + + @BeforeClass + public static void setUpClass() throws Exception { + Cloudinary cloudinary = new Cloudinary(); + if (cloudinary.config.apiSecret == null) { + System.err.println("Please setup environment for Upload test to run"); + } + } + + private static Map uploadResource(String publicId) throws IOException { + return new Cloudinary().uploader().upload(SRC_TEST_IMAGE, + asMap( "public_id", publicId, + "tags", new String[]{SDK_TEST_TAG, CONTEXT_TAG}, + "context", CONTEXT, + "transformation", new Transformation().crop("scale").width(10))); + } + + @AfterClass + public static void tearDownClass() { + Cloudinary cloudinary = new Cloudinary(); + try { + cloudinary.api().deleteResourcesByTag(CONTEXT_TAG, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + } + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() throws Exception { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + cloudinary = new Cloudinary(); + uploader = cloudinary.uploader(); + assumeNotNull(cloudinary.config.apiSecret); + + } + + @Test + public void testExplicit() throws Exception { + String publicId = "explicit_id" + SUFFIX; + uploadResource(publicId); + //should allow sending context + Map differentContext = asMap("caption", "different = caption", "alt2", "alt|alternative alternative"); + Map result = uploader.explicit(publicId, asMap("type", "upload", "context", differentContext)); + assertEquals("explicit API should return the new context", asMap("custom", differentContext), result.get("context")); + Map resource = cloudinary.api().resource(publicId, asMap("context", true)); + assertEquals("explicit API should replace the context", asMap("custom", differentContext), resource.get("context")); + } + + @Test + public void testAddContext() throws Exception { + String publicId = "add_context_id" + SUFFIX; + Map resource = uploadResource(publicId); + Map context = new HashMap((Map)((Map)resource.get("context")).get("custom")); + context.put("caption", "new caption"); + Map result = uploader.addContext(asMap("caption", "new caption"), new String[]{publicId, "no-such-id"}, null); + assertThat("addContext should return a list of modified public IDs", (List) result.get("public_ids"), contains(publicId)); + + resource = cloudinary.api().resource(publicId, asMap("context", true)); + assertEquals(asMap("custom", context), resource.get("context")); + } + + @Test + public void testRemoveAllContext() throws Exception { + String publicId = "remove_context_id" + SUFFIX; + uploadResource(publicId); + Map result = uploader.removeAllContext(new String[]{publicId, "no-such-id"}, null); + assertThat((List) result.get("public_ids"), contains(publicId)); + + Map resource = cloudinary.api().resource(publicId, asMap("context", true)); + assertThat((Map)resource, not(hasKey("context"))); + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractFoldersApiTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractFoldersApiTest.java new file mode 100644 index 00000000..a8835046 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractFoldersApiTest.java @@ -0,0 +1,101 @@ +package com.cloudinary.test; + +import com.cloudinary.Api; +import com.cloudinary.Cloudinary; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.utils.ObjectUtils; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import java.util.List; + +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +@SuppressWarnings({"rawtypes"}) +abstract public class AbstractFoldersApiTest extends MockableTest { + protected Api api; + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + assumeNotNull(cloudinary.config.apiSecret); + this.api = cloudinary.api(); + } + + @Test + public void testRootFolderWithParams() throws Exception { + String rootFolder1Name = "rootFolderWithParamsTest1" + SUFFIX; + assertTrue((Boolean) api.createFolder(rootFolder1Name, null).get("success")); + + String rootFolder2Name = "rootFolderWithParamsTest2" + SUFFIX; + assertTrue((Boolean) api.createFolder(rootFolder2Name, null).get("success")); + + Thread.sleep(2000); + + ApiResponse rootResponse1 = api.rootFolders(ObjectUtils.asMap("max_results", 1)); + List rootFolders1 = (List) rootResponse1.get("folders"); + assertNotNull(rootFolders1); + assertEquals(1, rootFolders1.size()); + + String nextCursor = (String) rootResponse1.get("next_cursor"); + assertNotNull(nextCursor); + + ApiResponse rootResponse2 = api.rootFolders(ObjectUtils.asMap("max_results", 1, "next_cursor", nextCursor)); + List folders2 = (List) rootResponse2.get("folders"); + assertNotNull(folders2); + assertEquals(1, folders2.size()); + + assertTrue(((List) api.deleteFolder(rootFolder1Name, null).get("deleted")).contains(rootFolder1Name)); + assertTrue(((List) api.deleteFolder(rootFolder2Name, null).get("deleted")).contains(rootFolder2Name)); + } + + @Test + public void testSubFolderWithParams() throws Exception { + String rootFolderName = "subfolderWithParamsTest" + SUFFIX; + assertTrue((Boolean) api.createFolder(rootFolderName, null).get("success")); + + String subFolder1Name = rootFolderName + "/subfolder1" + SUFFIX; + assertTrue((Boolean) api.createFolder(subFolder1Name, null).get("success")); + + String subFolder2Name = rootFolderName + "/subfolder2" + SUFFIX; + assertTrue((Boolean) api.createFolder(subFolder2Name, null).get("success")); + + Thread.sleep(2000); + + ApiResponse response = api.subFolders(rootFolderName, ObjectUtils.asMap("max_results", 1)); + List folders = (List) response.get("folders"); + assertNotNull(folders); + assertEquals(1, folders.size()); + + String nextCursor = (String) response.get("next_cursor"); + assertNotNull(nextCursor); + + ApiResponse response2 = api.subFolders(rootFolderName, ObjectUtils.asMap("max_results", 1, "next_cursor", nextCursor)); + List folders2 = (List) response2.get("folders"); + assertNotNull(folders2); + assertEquals(1, folders2.size()); + + ApiResponse result = api.deleteFolder(rootFolderName, null); + assertTrue(((List) result.get("deleted")).contains(rootFolderName)); + } + + @Test + public void testDeleteFolderWithSkipBackup() throws Exception { + //Create + String rootFolderName = "deleteFolderWithSkipBackup" + SUFFIX; + assertTrue((Boolean) api.createFolder(rootFolderName, null).get("success")); + + //Delete + ApiResponse result = api.deleteFolder(rootFolderName, ObjectUtils.asMap("skip_backup", "true")); + assertTrue(((List) result.get("deleted")).contains(rootFolderName)); + + + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractSearchTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractSearchTest.java new file mode 100644 index 00000000..e6bf5d6e --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractSearchTest.java @@ -0,0 +1,189 @@ +package com.cloudinary.test; + +import com.cloudinary.Cloudinary; +import com.cloudinary.Search; +import com.cloudinary.utils.ObjectUtils; +import org.junit.*; +import org.junit.rules.TestName; + +import java.lang.reflect.Field; +import java.util.*; + +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasItem; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +@SuppressWarnings({"rawtypes", "unchecked", "JavaDoc"}) +abstract public class AbstractSearchTest extends MockableTest { + @Rule + public TestName currentTest = new TestName(); + private static final String SEARCH_TAG = "search_test_tag_" + SUFFIX; + public static final String[] UPLOAD_TAGS = {SDK_TEST_TAG, SEARCH_TAG}; + private static final String SEARCH_TEST = "search_test_" + SUFFIX; + private static final String SEARCH_FOLDER = "search_folder_" + SUFFIX; + private static final String SEARCH_TEST_1 = SEARCH_TEST + "_1"; + private static final String SEARCH_TEST_2 = SEARCH_TEST + "_2"; + private static String SEARCH_TEST_ASSET_ID_1; + + @BeforeClass + public static void setUpClass() throws Exception { + Cloudinary cloudinary = new Cloudinary(); + Map options = ObjectUtils.asMap("public_id", SEARCH_TEST, "tags", UPLOAD_TAGS, "context", "stage=in_review"); + cloudinary.api().deleteResourcesByTag(SEARCH_TAG, null); + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + options = ObjectUtils.asMap("public_id", SEARCH_TEST_1, "tags", UPLOAD_TAGS, "context", "stage=new"); + SEARCH_TEST_ASSET_ID_1 = cloudinary.uploader().upload(SRC_TEST_IMAGE, options).get("asset_id").toString(); + options = ObjectUtils.asMap("public_id", SEARCH_TEST_2, "tags", UPLOAD_TAGS, "context", "stage=validated"); + cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + try { + Thread.sleep(5000); //wait for search indexing + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @AfterClass + public static void tearDownClass() throws Exception { + Cloudinary cloudinary = new Cloudinary(); + cloudinary.api().deleteResourcesByTag(SEARCH_TAG, null); + try { + cloudinary.api().deleteFolder(SEARCH_FOLDER, null); + } catch (Exception e){ + System.err.println(e.getMessage()); + } + } + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + assumeNotNull(cloudinary.config.apiSecret); + } + + @Test + public void shouldFindResourcesByTag() throws Exception { + Map result = cloudinary.search().expression(String.format("tags:%s", SEARCH_TAG)).execute(); + List resources = (List) result.get("resources"); + assertEquals(3, resources.size()); + } + + @Test + public void shouldFindFolders() throws Exception { + Map createFolderResult = cloudinary.api().createFolder(SEARCH_FOLDER, null); + Thread.sleep(3000); + if ((Boolean) createFolderResult.get("success")) { + Map result = cloudinary.searchFolders().expression(String.format("name:%s", SEARCH_FOLDER)).execute(); + System.out.println("SUCCESS!"); + final List folders = (List) result.get("folders"); + assertThat(folders, hasItem(hasEntry("name", SEARCH_FOLDER))); + } + } + + @Test + public void shouldFindResourceByPublicId() throws Exception { + Map result = cloudinary.search().expression(String.format("public_id:%s", SEARCH_TEST_1)).execute(); + List resources = (List) result.get("resources"); + assertEquals(1, resources.size()); + } + + @Test + public void shouldFindResourceByAssetId() throws Exception { + Map result = cloudinary.search().expression(String.format("asset_id:%s", SEARCH_TEST_ASSET_ID_1)).execute(); + List resources = (List) result.get("resources"); + assertEquals(1, resources.size()); + } + + @Test + public void testShouldNotDuplicateValues() throws Exception { + Search request = cloudinary.search().maxResults(1). + sortBy("created_at", "asc") + .sortBy("created_at", "desc") + .sortBy("public_id", "asc") + .aggregate("format") + .aggregate("format") + .aggregate("resource_type") + .withField("context") + .withField("context") + .withField("tags"); + Field[] fields = Search.class.getDeclaredFields(); + for(Field field : fields) { + if(field.getName() == "aggregateParam") { + field.setAccessible(true); + ArrayList aggregateList = (ArrayList) field.get(request); + Set testSet = new HashSet(aggregateList); + assertTrue(aggregateList.size() == testSet.size()); + } + if (field.getName() == "withFieldParam") { + field.setAccessible(true); + ArrayList withFieldList = (ArrayList) field.get(request); + Set testSet = new HashSet(withFieldList); + assertTrue(withFieldList.size() == testSet.size()); + } + if (field.getName() == "sortByParam") { + field.setAccessible(true); + ArrayList> sortByList = (ArrayList>) field.get(request); + Set> testSet = new HashSet>(sortByList); + assertTrue(sortByList.size() == testSet.size()); + } + } + } + + @Test + public void shouldPaginateResourcesLimitedByTagAndOrderdByAscendingPublicId() throws Exception { + List resources; + Map result = cloudinary.search().maxResults(1).expression(String.format("tags:%s", SEARCH_TAG)).sortBy("public_id", "asc").execute(); + resources = (List) result.get("resources"); + assertEquals(1, resources.size()); + assertEquals(3, result.get("total_count")); + assertEquals(SEARCH_TEST, resources.get(0).get("public_id")); + + + result = cloudinary.search().maxResults(1).expression(String.format("tags:%s", SEARCH_TAG)).sortBy("public_id", "asc") + .nextCursor(ObjectUtils.asString(result.get("next_cursor"))).execute(); + resources = (List) result.get("resources"); + + assertEquals(1, resources.size()); + assertEquals(3, result.get("total_count")); + assertEquals(SEARCH_TEST_1, resources.get(0).get("public_id")); + + result = cloudinary.search().maxResults(1).expression(String.format("tags:%s", SEARCH_TAG)).sortBy("public_id", "asc") + .nextCursor(ObjectUtils.asString(result.get("next_cursor"))).execute(); + resources = (List) result.get("resources"); + + assertEquals(1, resources.size()); + assertEquals(3, result.get("total_count")); + assertEquals(SEARCH_TEST_2, resources.get(0).get("public_id")); + assertNull(result.get("next_cursor")); + } + + @Test + public void testShouldBuildSearchUrl() throws Exception { + String nextCursor = "db27cfb02b3f69cb39049969c23ca430c6d33d5a3a7c3ad1d870c54e1a54ee0faa5acdd9f6d288666986001711759d10"; + Cloudinary cloudinaryToSearch = new Cloudinary("cloudinary://key:secret@test123"); + cloudinaryToSearch.config.secure = true; + + Search search = cloudinaryToSearch.search().expression("resource_type:image AND tags=kitten AND uploaded_at>1d AND bytes>1m").sortBy("public_id", "desc").maxResults(30); + String base64Query = "eyJleHByZXNzaW9uIjoicmVzb3VyY2VfdHlwZTppbWFnZSBBTkQgdGFncz1raXR0ZW4gQU5EIHVwbG9hZGVkX2F0PjFkIEFORCBieXRlcz4xbSIsIm1heF9yZXN1bHRzIjozMCwic29ydF9ieSI6W3sicHVibGljX2lkIjoiZGVzYyJ9XX0="; + String ttl300Signature = "431454b74cefa342e2f03e2d589b2e901babb8db6e6b149abf25bc0dd7ab20b7"; + String ttl1000Signature = "25b91426a37d4f633a9b34383c63889ff8952e7ffecef29a17d600eeb3db0db7"; + + assertEquals(String.format("https://res.cloudinary.com/%s/search/%s/%d/%s", cloudinaryToSearch.config.cloudName, ttl300Signature, 300, base64Query), search.toUrl()); + assertEquals(String.format("https://res.cloudinary.com/%s/search/%s/%d/%s/%s", cloudinaryToSearch.config.cloudName, ttl300Signature, 300, base64Query, nextCursor), search.toUrl(nextCursor)); + assertEquals(String.format("https://res.cloudinary.com/%s/search/%s/%d/%s/%s", cloudinaryToSearch.config.cloudName, ttl1000Signature, 1000, base64Query, nextCursor), search.toUrl(1000, nextCursor)); + cloudinaryToSearch.config.privateCdn = true; + assertEquals(String.format("https://%s-res.cloudinary.com/search/%s/%d/%s", cloudinaryToSearch.config.cloudName, ttl300Signature, 300, base64Query), search.toUrl(300, "")); + } + + @Test + public void testSearchWithSelectiveResponse() throws Exception { + Map result = cloudinary.search().expression(String.format("tags:%s", SEARCH_TAG)).fields("width").fields("height").execute(); + List resources = (List) result.get("resources"); + assertEquals(3, resources.size()); + Map resource = resources.get(0); + assertNotNull(resource); + assertNotNull(resource.get("width")); + assertNotNull(resource.get("height")); + assertNull(resource.get("format")); + } +} \ No newline at end of file diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStreamingProfilesApiTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStreamingProfilesApiTest.java new file mode 100644 index 00000000..6a917208 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStreamingProfilesApiTest.java @@ -0,0 +1,145 @@ +package com.cloudinary.test; + +import com.cloudinary.Api; +import com.cloudinary.Cloudinary; +import com.cloudinary.Transformation; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.exceptions.AlreadyExists; +import com.cloudinary.api.exceptions.NotFound; +import com.cloudinary.utils.ObjectUtils; +import org.hamcrest.Matcher; +import org.hamcrest.Matchers; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +abstract public class AbstractStreamingProfilesApiTest extends MockableTest { + private static final String PROFILE_NAME = "api_test_streaming_profile" + SUFFIX; + protected Api api; + private static final List PREDEFINED_PROFILES = Arrays.asList("4k", "full_hd", "hd", "sd", "full_hd_wifi", "full_hd_lean", "hd_lean"); + public static final String UPDATE_PROFILE_NAME = PROFILE_NAME + "_update"; + public static final String DELETE_PROFILE_NAME = PROFILE_NAME + "_delete"; + public static final String CREATE_PROFILE_NAME = PROFILE_NAME + "_create"; + + @BeforeClass + public static void setUpClass() throws IOException { + Cloudinary cloudinary = new Cloudinary(); + if (cloudinary.config.apiSecret == null) { + System.err.println("Please setup environment for Upload test to run"); + } + } + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + assumeNotNull(cloudinary.config.apiSecret); + this.api = cloudinary.api(); + } + + @Test + public void testCreate() throws Exception { + ApiResponse result = api.createStreamingProfile(CREATE_PROFILE_NAME, null, Collections.singletonList(ObjectUtils.asMap( + "transformation", new Transformation().crop("limit").width(1200).height(1200).bitRate("5m") + )), ObjectUtils.emptyMap()); + + assertTrue(result.containsKey("data")); + Map profile = (Map) result.get("data"); + assertThat(profile, (Matcher) hasEntry("name", (Object) CREATE_PROFILE_NAME)); + } + + @Test + public void testGet() throws Exception { + ApiResponse result = api.getStreamingProfile(PREDEFINED_PROFILES.get(0)); + assertTrue(result.containsKey("data")); + Map profile = (Map) result.get("data"); + assertThat(profile, (Matcher) hasEntry("name", (Object) (PREDEFINED_PROFILES.get(0)))); + + } + + @Test + public void testList() throws Exception { + ApiResponse result = api.listStreamingProfiles(); + assertTrue(result.containsKey("data")); + List profiles = (List) result.get("data"); + // check that the list contains all predefined profiles + for (String p : + PREDEFINED_PROFILES) { + assertThat(profiles, (Matcher) hasItem(hasEntry("name", p))); + } + } + + @Test + public void testDelete() throws Exception { + ApiResponse result; + try { + api.createStreamingProfile(DELETE_PROFILE_NAME, null, Collections.singletonList(ObjectUtils.asMap( + "transformation", new Transformation().crop("limit").width(1200).height(1200).bitRate("5m") + )), ObjectUtils.emptyMap()); + } catch (AlreadyExists ignored) { + } + + result = api.deleteStreamingProfile(DELETE_PROFILE_NAME); + assertEquals("deleted", result.get("message")); + } + + @Test + public void testUpdate() throws Exception { + try { + api.createStreamingProfile(UPDATE_PROFILE_NAME, null, Collections.singletonList(ObjectUtils.asMap( + "transformation", new Transformation().crop("limit").width(1200).height(1200).bitRate("5m") + )), ObjectUtils.emptyMap()); + } catch (AlreadyExists ignored) { + } + Map result = api.updateStreamingProfile(UPDATE_PROFILE_NAME, null, Collections.singletonList( + ObjectUtils.asMap("transformation", + new Transformation().crop("limit").width(800).height(800).bitRate("5m") + )), ObjectUtils.emptyMap()); + + assertTrue(result.containsKey("data")); + assertThat(result, (Matcher) hasEntry("message", (Object) "updated")); + Map profile = (Map) result.get("data"); + assertThat(profile, (Matcher) hasEntry("name", (Object) UPDATE_PROFILE_NAME)); + assertThat(profile, Matchers.hasEntry(equalTo("representations"), (Matcher) hasItem(hasKey("transformation")))); + final Map representation = (Map) ((List) profile.get("representations")).get(0); + Map transformation = (Map) ((List) representation.get("transformation")).get(0); + assertThat(transformation, allOf( + (Matcher) hasEntry("width", 800), + (Matcher) hasEntry("height", 800), + (Matcher) hasEntry("crop", "limit"), + (Matcher) hasEntry("bit_rate", "5m") + )); + } + + @AfterClass + public static void tearDownClass() throws Exception { + Api api = new Cloudinary().api(); + try { + api.deleteStreamingProfile(CREATE_PROFILE_NAME); + } catch (NotFound ignored) { + } + + try { + api.deleteStreamingProfile(UPDATE_PROFILE_NAME); + } catch (NotFound ignored) { + } + + try { + // this should already be gone but in case that deletion-test failed we still need to cleanup the account. + api.deleteStreamingProfile(DELETE_PROFILE_NAME); + } catch (NotFound ignored) { + } + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStructuredMetadataTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStructuredMetadataTest.java new file mode 100644 index 00000000..b1137fb4 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractStructuredMetadataTest.java @@ -0,0 +1,402 @@ +package com.cloudinary.test; + +import com.cloudinary.Api; +import com.cloudinary.Cloudinary; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.api.exceptions.BadRequest; +import com.cloudinary.metadata.*; + +import com.cloudinary.test.helpers.Feature; +import com.cloudinary.utils.ObjectUtils; +import org.hamcrest.Matchers; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.IOException; +import java.util.*; + +import static com.cloudinary.utils.ObjectUtils.asMap; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +public abstract class AbstractStructuredMetadataTest extends MockableTest { + private static final String METADATA_UPLOADER_TAG = SDK_TEST_TAG + "_uploader"; + private static final String PUBLIC_ID = "before_class_public_id" + SUFFIX; + private static final String PRIVATE_PUBLIC_ID = "before_class_private_public_id" + SUFFIX; + protected Api api; + public static final List metadataFieldExternalIds = new ArrayList(); + + @BeforeClass + public static void setUpClass() throws IOException { + Cloudinary cloudinary = new Cloudinary(); + if (cloudinary.config.apiSecret == null) { + System.err.println("Please setup environment for Upload test to run"); + } + + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("public_id", PUBLIC_ID)); + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("public_id", PRIVATE_PUBLIC_ID, "type", "private")); + } + + @AfterClass + public static void tearDownClass() throws Exception { + Api api = new Cloudinary().api(); + + for (String externalId : metadataFieldExternalIds) { + try { + api.deleteMetadataField(externalId); + } catch (Exception ignored) { + } + } + } + + @Rule + public TestName currentTest = new TestName(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + assumeNotNull(cloudinary.config.apiSecret); + this.api = cloudinary.api(); + } + + @Test + public void testCreateMetadata() throws Exception { + StringMetadataField stringField = newFieldInstance("testCreateMetadata_1", true); + ApiResponse result = addFieldToAccount(stringField); + assertNotNull(result); + assertEquals(stringField.getLabel(), result.get("label")); + + SetMetadataField setField = createSetField("testCreateMetadata_2"); + result = cloudinary.api().addMetadataField(setField); + assertNotNull(result); + assertEquals(setField.getLabel(), result.get("label")); + } + + @Test + public void testCreateSetMetadataWithAllowDynamicListValues() throws Exception { + SetMetadataField setField = createSetField("testCreateMetadata_4"); + ApiResponse result = cloudinary.api().addMetadataField(setField); + assertNotNull(result); + assertEquals(setField.getLabel(), result.get("label")); + assertEquals(true, result.get("allow_dynamic_list_values")); + } + + @Test + public void testFieldRestrictions() throws Exception { + StringMetadataField stringField = newFieldInstance("testCreateMetadata_3", true); + stringField.setRestrictions(new Restrictions().setReadOnlyUI()); + + ApiResponse result = api.addMetadataField(stringField); + assertNotNull(result); + Map restrictions = (Map) result.get("restrictions"); + assertNotNull(restrictions); + assertTrue((Boolean) restrictions.get("readonly_ui")); + } + + @Test + public void testDateFieldDefaultValueValidation() throws Exception { + // now minus 3 days hours. + Date max = new Date(); + Date min = new Date(max.getTime() - 72 * 60 * 60 * 1000); + + Date legalValue = new Date(min.getTime() + 36 * 60 * 60 * 1000); + Date illegalValue = new Date(max.getTime() + 36 * 60 * 60 * 1000); + + DateMetadataField dateMetadataField = new DateMetadataField(); + dateMetadataField.setLabel("Start date" + new Date().getTime()); + + List rules = new ArrayList(); + rules.add(new MetadataValidation.DateGreaterThan(min)); + rules.add(new MetadataValidation.DateLessThan(max)); + dateMetadataField.setValidation(new MetadataValidation.AndValidator(rules)); + + String message = null; + ApiResponse res = null; + try { + // should fail + dateMetadataField.setDefaultValue(illegalValue); + res = api.addMetadataField(dateMetadataField); + // this line should not be reached if all is working well, but when it's not we still want to clean it up: + metadataFieldExternalIds.add(res.get("external_id").toString()); + } catch (BadRequest e) { + message = e.getMessage(); + } + + assertEquals(message, "default_value is invalid"); + + // should work: + dateMetadataField.setDefaultValue(legalValue); + res = api.addMetadataField(dateMetadataField); + metadataFieldExternalIds.add(res.get("external_id").toString()); + } + + @Test + public void testListFields() throws Exception { + StringMetadataField stringField = newFieldInstance("testListFields", true); + addFieldToAccount(stringField); + + ApiResponse result = cloudinary.api().listMetadataFields(); + assertNotNull(result); + assertNotNull(result.get("metadata_fields")); + assertTrue(((List)result.get("metadata_fields")).size() > 0); + } + + @Test + public void testGetMetadata() throws Exception { + ApiResponse fieldResult = addFieldToAccount(newFieldInstance("testGetMetadata", true)); + ApiResponse result = api.metadataFieldByFieldId(fieldResult.get("external_id").toString()); + assertNotNull(result); + assertEquals(fieldResult.get("label"), result.get("label")); + } + + @Test + public void testUpdateField() throws Exception { + StringMetadataField metadataField = newFieldInstance("testUpdateField", false); + ApiResponse fieldResult = addFieldToAccount(metadataField); + assertNotEquals("new_def", fieldResult.get("default_value")); + metadataField.setDefaultValue("new_def"); + metadataField.setDefaultDisabled(true); + metadataField.setRestrictions(new Restrictions().setReadOnlyUI()); + ApiResponse result = api.updateMetadataField(fieldResult.get("external_id").toString(), metadataField); + assertNotNull(result); + assertEquals("new_def", result.get("default_value")); + assertEquals(true, result.get("default_disabled")); + Map restrictions = (Map) result.get("restrictions"); + assertNotNull(restrictions); + assertTrue((Boolean)restrictions.get("readonly_ui")); + } + + @Test + public void testDeleteField() throws Exception { + ApiResponse fieldResult = addFieldToAccount(newFieldInstance("testDeleteField", true)); + ApiResponse result = api.deleteMetadataField(fieldResult.get("external_id").toString()); + assertNotNull(result); + assertEquals("ok", result.get("message")); + } + + @Test + public void testUpdateDatasource() throws Exception { + SetMetadataField setField = createSetField("testUpdateDatasource"); + ApiResponse fieldResult = addFieldToAccount(setField); + MetadataDataSource.Entry newEntry = new MetadataDataSource.Entry("id1", "new1"); + ApiResponse result = api.updateMetadataFieldDatasource(fieldResult.get("external_id").toString(), Collections.singletonList(newEntry)); + assertNotNull(result); + assertEquals("new1", ((Map) ((List) result.get("values")).get(0)).get("value")); + } + + @Test + public void testDeleteDatasourceEntries() throws Exception { + SetMetadataField setField = createSetField("testDeleteDatasourceEntries"); + ApiResponse fieldResult = addFieldToAccount(setField); + ApiResponse result = api.deleteDatasourceEntries(fieldResult.get("external_id").toString(), Collections.singletonList("id1")); + assertNotNull(result); + } + + @Test + public void testRestoreDatasourceEntries() throws Exception { + SetMetadataField setField = createSetField("testRestoreDatasourceEntries"); + ApiResponse fieldResult = addFieldToAccount(setField); + String fieldExternalId = fieldResult.get("external_id").toString(); + api.deleteDatasourceEntries(fieldExternalId, Collections.singletonList("id1")); + ApiResponse result = api.restoreDatasourceEntries(fieldExternalId, Collections.singletonList("id1")); + assertNotNull(result); + } + + @Test + public void testReorderMetadataFieldsByLabel() throws Exception { + AddStringField("some_value"); + AddStringField("aaa"); + AddStringField("zzz"); + + ApiResponse result = api.reorderMetadataFields("label", null, Collections.EMPTY_MAP); + assertThat(getField(result, 0), Matchers.containsString("aaa")); + + result = api.reorderMetadataFields("label", "desc", Collections.EMPTY_MAP); + assertThat(getField(result, 0), Matchers.containsString("zzz")); + + result = api.reorderMetadataFields("label", "asc", Collections.EMPTY_MAP); + assertThat(getField(result, 0), Matchers.containsString("aaa")); + } + + @Test(expected = IllegalArgumentException.class) + public void testReorderMetadataFieldsOrderByIsRequired() throws Exception { + api.reorderMetadataFields(null, null, Collections.EMPTY_MAP); + } + + private String getField(ApiResponse result, int index) { + String actual = ((Map)((ArrayList)result.get("metadata_fields")).get(index)).get("label").toString(); + return actual; + } + + private void AddStringField(String labelPrefix) throws Exception { + StringMetadataField field = newFieldInstance(labelPrefix, true); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + } + + @Test + public void testUploadWithMetadata() throws Exception { + StringMetadataField field = newFieldInstance("testUploadWithMetadata", true); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("metadata", metadata, "tags", Arrays.asList(SDK_TEST_TAG, METADATA_UPLOADER_TAG))); + assertNotNull(result.get("metadata")); + assertEquals("123456", ((Map) result.get("metadata")).get(fieldId)); + } + + @Test + public void testExplicitWithMetadata() throws Exception { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, METADATA_UPLOADER_TAG))); + String publicId = uploadResult.get("public_id").toString(); + StringMetadataField field = newFieldInstance("testExplicitWithMetadata", true); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.uploader().explicit(publicId, asMap("type", "upload", "resource_type", "image", "metadata", metadata)); + assertNotNull(result.get("metadata")); + assertEquals("123456", ((Map) result.get("metadata")).get(fieldId)); + + // explicit with invalid data, should fail: + metadata = Collections.singletonMap(fieldId, "12"); + String message = ""; + try { + result = cloudinary.uploader().explicit(publicId, asMap("type", "upload", "resource_type", "image", "metadata", metadata)); + } catch (Exception e){ + message = e.getMessage(); + } + + assertTrue(message.contains("is not valid for field") ); + } + + @Test + public void testUpdateWithMetadata() throws Exception { + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, METADATA_UPLOADER_TAG))); + String publicId = uploadResult.get("public_id").toString(); + StringMetadataField field = newFieldInstance("testUpdateWithMetadata", true); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.api().update(publicId, asMap("type", "upload", "resource_type", "image", "metadata", metadata)); + assertNotNull(result.get("metadata")); + assertEquals("123456", ((Map) result.get("metadata")).get(fieldId)); + } + + @Test + public void testUploaderUpdateMetadata() throws Exception { + StringMetadataField field = newFieldInstance("testUploaderUpdateMetadata", true); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map result = cloudinary.uploader().updateMetadata(Collections.singletonMap(fieldId, "123456"), new String[]{PUBLIC_ID}, null); + assertNotNull(result); + assertEquals(PUBLIC_ID, ((List) result.get("public_ids")).get(0).toString()); + //test updateMetadata for private asset + Map result2 = cloudinary.uploader().updateMetadata(Collections.singletonMap(fieldId, "123456"), new String[]{PRIVATE_PUBLIC_ID}, asMap("type","private")); + assertNotNull(result); + assertEquals(PRIVATE_PUBLIC_ID, ((List) result2.get("public_ids")).get(0).toString()); + } + + @Test + public void testUploaderUpdateMetadataClearInvalid() throws Exception { + StringMetadataField field = newFieldInstance("testUploaderUpdateMetadata1", true); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map result = cloudinary.uploader().updateMetadata(Collections.singletonMap(fieldId, "123456"), new String[]{PUBLIC_ID}, ObjectUtils.asMap("clear_invalid", true)); + assertNotNull(result); + } + + @Test + public void testSetField() throws Exception { + SetMetadataField field = createSetField("test123"); + ApiResponse fieldResult = addFieldToAccount(field); + String fieldId = fieldResult.get("external_id").toString(); + Map result = cloudinary.uploader().updateMetadata(asMap(fieldId, new String[]{"id2", "id3"}), new String[]{PUBLIC_ID}, null); + assertNotNull(result); + assertEquals(PUBLIC_ID, ((List) result.get("public_ids")).get(0).toString()); + List list = new ArrayList(2); + list.add("id1"); + list.add("id2"); + result = cloudinary.uploader().updateMetadata(asMap(fieldId, list), new String[]{PUBLIC_ID}, null); + assertNotNull(result); + assertEquals(PUBLIC_ID, ((List) result.get("public_ids")).get(0).toString()); + } + + @Test + public void testListMetadataRules() throws Exception { + Assume.assumeTrue(MockableTest.shouldTestFeature(Feature.CONDITIONAL_METADATA_RULES)); + ApiResponse result = cloudinary.api().listMetadataRules(null); + assertNotNull(result); + } + + @Test + public void testAddMetadataRule() throws Exception { + Assume.assumeTrue(MockableTest.shouldTestFeature(Feature.CONDITIONAL_METADATA_RULES)); + SetMetadataField field = createSetField("test123"); + ApiResponse response = addFieldToAccount(field); + assertNotNull(response); + + String externalId = (String) response.get("external_id"); + MetadataRule rule = new MetadataRule(externalId, "category-employee", new MetadataRuleCondition("category", false, null, "employee"), new MetadataRuleResult(true, "all", null, null)); + ApiResponse result = cloudinary.api().addMetadataRule(rule, ObjectUtils.asMap()); + assertNotNull(result); + + String name = (String) result.get("name"); + assertEquals(name, "category-employee"); + } + + @Test + public void testUpdateMetadataRule() throws Exception { + Assume.assumeTrue(MockableTest.shouldTestFeature(Feature.CONDITIONAL_METADATA_RULES)); + ApiResponse response = cloudinary.api().listMetadataRules(null); + List metadataRules = (List) response.get("metadata_rules"); + assertNotNull(metadataRules); + String externalId = (String) ((Map) metadataRules.get(0)).get("external_id"); + + MetadataRule rule = new MetadataRule(null, "test_name", null, null); + ApiResponse result = cloudinary.api().updateMetadataRule(externalId, rule, ObjectUtils.asMap()); + assertNotNull(result); + } + + @Test + public void testDeleteMetadataRule() throws Exception { + Assume.assumeTrue(MockableTest.shouldTestFeature(Feature.CONDITIONAL_METADATA_RULES)); + ApiResponse response = cloudinary.api().listMetadataRules(null); + List metadataRules = (List) response.get("metadata_rules"); + assertNotNull(metadataRules); + String externalId = (String) ((Map) metadataRules.get(0)).get("external_id"); + + ApiResponse result = cloudinary.api().deleteMetadataRule(externalId, ObjectUtils.emptyMap()); + assertNotNull(result); + } + + // Metadata test helpers + private SetMetadataField createSetField(String labelPrefix) { + SetMetadataField setField = new SetMetadataField(); + String label = labelPrefix + "_" + SUFFIX; + setField.setLabel(label); + setField.setMandatory(false); + setField.setAllowDynamicListValues(true); + setField.setValidation(new MetadataValidation.StringLength(3, 99)); + setField.setDefaultValue(Arrays.asList("id2", "id3")); + setField.setValidation(null); + List entries = new ArrayList(); + entries.add(new MetadataDataSource.Entry("id1", "first_value")); + entries.add(new MetadataDataSource.Entry("id2", "second_value")); + entries.add(new MetadataDataSource.Entry("id3", "third_value")); + MetadataDataSource dataSource = new MetadataDataSource(entries); + setField.setDataSource(dataSource); + return setField; + } + + private StringMetadataField newFieldInstance(String labelPrefix, Boolean mandatory) throws Exception { + String label = labelPrefix + "_" + SUFFIX; + return MetadataTestHelper.newFieldInstance(label, mandatory); + } + + private ApiResponse addFieldToAccount(MetadataField field) throws Exception { + ApiResponse apiResponse = MetadataTestHelper.addFieldToAccount(api, field); + metadataFieldExternalIds.add(apiResponse.get("external_id").toString()); + return apiResponse; + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java new file mode 100644 index 00000000..794c926a --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/AbstractUploaderTest.java @@ -0,0 +1,858 @@ +package com.cloudinary.test; + +import com.cloudinary.*; +import com.cloudinary.metadata.StringMetadataField; +import com.cloudinary.test.rules.RetryRule; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.Rectangle; +import org.cloudinary.json.JSONArray; +import org.junit.*; +import org.junit.rules.TestName; + +import java.io.*; +import java.net.HttpURLConnection; +import java.net.URL; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.*; +import java.util.zip.ZipInputStream; + +import static com.cloudinary.utils.ObjectUtils.*; +import static com.cloudinary.utils.StringUtils.isRemoteUrl; +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeNotNull; + +@SuppressWarnings({"rawtypes", "unchecked"}) +abstract public class AbstractUploaderTest extends MockableTest { + private static final String ARCHIVE_TAG = SDK_TEST_TAG + "_archive"; + private static final String UPLOADER_TAG = SDK_TEST_TAG + "_uploader"; + public static final int SRC_TEST_IMAGE_W = 241; + public static final int SRC_TEST_IMAGE_H = 51; + private static Map> toDelete = new HashMap>(); + private static final String UPLOADER_TEST_PUBLIC_ID = "uploader_test"; + public static final String SRC_FULLY_QUALIFIED_IMAGE="image/upload/" + UPLOADER_TEST_PUBLIC_ID; + public static final String SRC_FULLY_QUALIFIED_VIDEO="video/upload/dog"; + public static final String SRC_TEST_EVAL= "if (resource_info['width'] < 450) { upload_options['quality_analysis'] = true };" + "upload_options['context'] = 'width=' + resource_info['width'];"; + + + @BeforeClass + public static void setUpClass() throws IOException { + Cloudinary cloudinary = new Cloudinary(); + cloudinary.config.analytics = false; + if (cloudinary.config.apiSecret == null) { + System.err.println("Please setup environment for Upload test to run"); + } + + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", new String[]{SDK_TEST_TAG, UPLOADER_TAG, ARCHIVE_TAG})); + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", new String[]{SDK_TEST_TAG, UPLOADER_TAG}, "public_id", UPLOADER_TEST_PUBLIC_ID, "transformation", "f_jpg")); + cloudinary.uploader().upload(SRC_TEST_VIDEO, asMap("tags", new String[]{SDK_TEST_TAG, UPLOADER_TAG, ARCHIVE_TAG}, "public_id", "dog", "resource_type", "video")); + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", new String[]{SDK_TEST_TAG, UPLOADER_TAG, ARCHIVE_TAG}, "resource_type", "raw")); + cloudinary.uploader().upload(SRC_TEST_IMAGE, + asMap("tags", new String[]{SDK_TEST_TAG, UPLOADER_TAG, ARCHIVE_TAG}, + "transformation", new Transformation().crop("scale").width(10))); + } + + @AfterClass + public static void tearDownClass() { + Api api = new Cloudinary().api(); + try { + api.deleteResourcesByTag(UPLOADER_TAG, ObjectUtils.emptyMap()); + } catch (Exception ignored) { + } + try { + api.deleteResourcesByTag(UPLOADER_TAG, ObjectUtils.asMap("resource_type", "video")); + } catch (Exception ignored) { + } + try { + api.deleteResourcesByTag(UPLOADER_TAG, ObjectUtils.asMap("resource_type", "raw")); + } catch (Exception ignored) { + } + for (String type : toDelete.keySet()) { + try { + api.deleteResources(toDelete.get(type), Collections.singletonMap("type", type)); + } catch (Exception ignored) { + } + } + + toDelete.clear(); + } + + @Rule + public TestName currentTest = new TestName(); + + @Rule + public RetryRule retryRule = new RetryRule(); + + @Before + public void setUp() { + System.out.println("Running " + this.getClass().getName() + "." + currentTest.getMethodName()); + this.cloudinary = new Cloudinary(); + this.cloudinary.config.analytics = false; + assumeNotNull(cloudinary.config.apiSecret); + } + + + @Test + public void testUtf8Upload() throws IOException { + + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("colors", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG), "public_id", "aåßéƒ")); + assertEquals(result.get("width"), SRC_TEST_IMAGE_W); + assertEquals(result.get("height"), SRC_TEST_IMAGE_H); + assertNotNull(result.get("colors")); + assertNotNull(result.get("predominant")); + Map to_sign = new HashMap(); + to_sign.put("public_id", result.get("public_id")); + to_sign.put("version", ObjectUtils.asString(result.get("version"))); + String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.config.apiSecret, cloudinary.config.signatureVersion); + assertEquals(result.get("signature"), expected_signature); + } + + @Test + public void testDeleteByToken() throws Exception { + Map options = ObjectUtils.asMap("return_delete_token", true, "tags", new String[]{SDK_TEST_TAG, UPLOADER_TAG}); + Map res = cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + String token = (String) res.get("delete_token"); + Map baseConfig = cloudinary.config.asMap(); + baseConfig.remove("api_key"); + baseConfig.remove("api_secret"); + res = new Cloudinary(baseConfig).uploader().deleteByToken(token); + assertNotNull(res); + assertEquals("ok", res.get("result")); + } + + @Test + public void testUpload() throws IOException { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("colors", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("width"), SRC_TEST_IMAGE_W); + assertEquals(result.get("height"), SRC_TEST_IMAGE_H); + assertNotNull(result.get("colors")); + assertNotNull(result.get("predominant")); + Map to_sign = new HashMap(); + to_sign.put("public_id", result.get("public_id")); + to_sign.put("version", ObjectUtils.asString(result.get("version"))); + String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.config.apiSecret, cloudinary.config.signatureVersion); + assertEquals(result.get("signature"), expected_signature); + } + + @Test + public void testIsRemoteUrl() { + String[] urls = new String[]{ + "ftp://ftp.cloudinary.com/images/old_logo.png", + "http://cloudinary.com/images/old_logo.png", + "https://cloudinary.com/images/old_logo.png", + "s3://s3-us-west-2.amazonaws.com/cloudinary/images/old_logo.png", + "gs://cloudinary/images/old_logo.png", + "data:image/gif;charset=utf8;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", + "data:image/gif;param1=value1;param2=value2;base64," + + "R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7", + "data:image/svg+xml;charset=utf-8;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg"}; + + for (String url : urls) { + assertTrue(isRemoteUrl(url)); + } + + String[] invalidUrls = new String[]{"adsadasdasdasd", " ", ""}; + + for (String url : invalidUrls) { + assertFalse(isRemoteUrl(url)); + } + } + + @Test + public void testUploadUrl() throws IOException { + Map result = cloudinary.uploader().upload(REMOTE_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("width"), SRC_TEST_IMAGE_W); + assertEquals(result.get("height"), SRC_TEST_IMAGE_H); + Map to_sign = new HashMap(); + to_sign.put("public_id", result.get("public_id")); + to_sign.put("version", ObjectUtils.asString(result.get("version"))); + String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.config.apiSecret, cloudinary.config.signatureVersion); + assertEquals(result.get("signature"), expected_signature); + } + + @Test + public void testUploadLargeUrl() throws IOException { + Map result = cloudinary.uploader().uploadLarge(REMOTE_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("width"), SRC_TEST_IMAGE_W); + assertEquals(result.get("height"), SRC_TEST_IMAGE_H); + Map to_sign = new HashMap(); + to_sign.put("public_id", result.get("public_id")); + to_sign.put("version", ObjectUtils.asString(result.get("version"))); + String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.config.apiSecret, cloudinary.config.signatureVersion); + assertEquals(result.get("signature"), expected_signature); + } + + @Test + public void testUploadDataUri() throws IOException { + Map result = cloudinary.uploader().upload("data:image/png;base64,iVBORw0KGgoAA\nAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABlBMVEUAAAD///+l2Z/dAAAAM0l\nEQVR4nGP4/5/h/1+G/58ZDrAz3D/McH8yw83NDDeNGe4Ug9C9zwz3gVLMDA/A6\nP9/AFGGFyjOXZtQAAAAAElFTkSuQmCC", asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("width"), 16); + assertEquals(result.get("height"), 16); + Map to_sign = new HashMap(); + to_sign.put("public_id", result.get("public_id")); + to_sign.put("version", ObjectUtils.asString(result.get("version"))); + String expected_signature = cloudinary.apiSignRequest(to_sign, cloudinary.config.apiSecret, cloudinary.config.signatureVersion); + assertEquals(result.get("signature"), expected_signature); + } + + @Test + public void testUploadUTF8() throws IOException { + Map result = cloudinary.uploader().upload("../cloudinary-test-common/src/main/resources/old_logo.png", asMap("public_id", "Plattenkreiss_ñg-é", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("public_id"), "Plattenkreiss_ñg-é"); + cloudinary.uploader().upload(result.get("url"), asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } + + @Test + public void testRename() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + + Object publicId = result.get("public_id"); + String publicId2 = "folder/" + publicId + "2"; + cloudinary.uploader().rename((String) publicId, publicId2, ObjectUtils.emptyMap()); + assertNotNull(cloudinary.api().resource(publicId2, ObjectUtils.emptyMap())); + + Map result2 = cloudinary.uploader().upload("../cloudinary-test-common/src/main/resources/favicon.ico", asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + boolean error_found = false; + try { + cloudinary.uploader().rename((String) result2.get("public_id"), publicId2, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } catch (Exception e) { + error_found = true; + } + assertTrue(error_found); + cloudinary.uploader().rename((String) result2.get("public_id"), publicId2, asMap("overwrite", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(cloudinary.api().resource(publicId2, ObjectUtils.emptyMap()).get("format"), "ico"); + } + + @Test + public void testRenameShouldReturnContext() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG), "context", asMap("foo", "boo"))); + + String publicId = result.get("public_id").toString(); + String publicId2 = "folder/" + publicId + "2"; + Map renameResult = cloudinary.uploader().rename(publicId, publicId2, asMap("context", true)); + assertNotNull(renameResult.get("context")); + } + + @Test + public void testRenameShouldReturnMetadata() throws Exception { + String label = "test" + SUFFIX; + StringMetadataField f = MetadataTestHelper.newFieldInstance(label, true); + Map fieldResult = MetadataTestHelper.addFieldToAccount(cloudinary.api(), f); + String fieldId = fieldResult.get("external_id").toString(); + Map metadata = Collections.singletonMap(fieldId, "123456"); + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG), "metadata", metadata)); + + String publicId = result.get("public_id").toString(); + String publicId2 = "folder/" + publicId + "2"; + Map renameResult = cloudinary.uploader().rename(publicId, publicId2, asMap("metadata", true)); + assertNotNull(renameResult.get("metadata")); + } + + @Test + public void testUniqueFilename() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("use_filename", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertTrue(((String) result.get("public_id")).matches("old_logo_[a-z0-9]{6}")); + result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("use_filename", true, "unique_filename", false, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("public_id"), "old_logo"); + } + + @Test + public void testExplicit() throws IOException { + Map result = cloudinary.uploader().explicit(UPLOADER_TEST_PUBLIC_ID, asMap("eager", Collections.singletonList(new Transformation().crop("scale").width(2.0)), "type", "upload", "moderation", "manual")); + String url = cloudinary.url().transformation(new Transformation().crop("scale").width(2.0)).format("jpg").version(result.get("version")).generate(UPLOADER_TEST_PUBLIC_ID); + String eagerUrl = (String) ((Map) ((List) result.get("eager")).get(0)).get("url"); + String cloudName = cloudinary.config.cloudName; + assertEquals(eagerUrl.substring(eagerUrl.indexOf(cloudName)), url.substring(url.indexOf(cloudName))); + } + + @Test + public void testEager() throws IOException { + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("eager", Collections.singletonList(new Transformation().crop("scale").width(2.0)), "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } + + @Test + public void testUploadAsync() throws IOException { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("transformation", new Transformation().crop("scale").width(2.0), "async", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals((String) result.get("status"), "pending"); + } + + @Test + public void testHeaders() throws IOException { + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("headers", new String[]{"Link: 1"}, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("headers", asMap("Link", "1"), "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } + + @Test + public void testText() throws Exception { + Map result = cloudinary.uploader().text("hello world", asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + addToDeleteList("text", result.get("public_id").toString()); + assertTrue(((Integer) result.get("width")) > 1); + assertTrue(((Integer) result.get("height")) > 1); + } + + @Test + public void testImageUploadTag() { + String tag = cloudinary.uploader().imageUploadTag("test-field", asMap("callback", "http://localhost/cloudinary_cors.html"), asMap("htmlattr", "htmlvalue")); + assertTrue(tag.contains("type='file'")); + assertTrue(tag.contains("data-cloudinary-field='test-field'")); + assertTrue(tag.contains("class='cloudinary-fileupload'")); + assertTrue(tag.contains("htmlattr='htmlvalue'")); + tag = cloudinary.uploader().imageUploadTag("test-field", asMap("callback", "http://localhost/cloudinary_cors.html"), asMap("class", "myclass")); + assertTrue(tag.contains("class='cloudinary-fileupload myclass'")); + } + + @Test + public void testEvalUploadParameter() throws IOException { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap( + "eval",SRC_TEST_EVAL, + "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG) + )); + assertTrue(result.get("quality_analysis")!=null && + ((HashMap)result.get("quality_analysis")).containsKey("focus")); + Map custom= (Map)((Map) result.get("context")).get("custom"); + assertEquals(custom.get("width"),Integer.toString(SRC_TEST_IMAGE_W)); + } + + @Test + public void testSprite() throws Exception { + final String sprite_test_tag = String.format("sprite_test_tag_%d", new java.util.Date().getTime()); + Map uploadResult1 = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", new String[]{sprite_test_tag, SDK_TEST_TAG, UPLOADER_TAG}, "public_id", "sprite_test_tag_1" + SUFFIX)); + Map uploadResult2 = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("tags", new String[]{sprite_test_tag, SDK_TEST_TAG, UPLOADER_TAG}, "public_id", "sprite_test_tag_2" + SUFFIX)); + + String[] urls = new String[]{uploadResult1.get("url").toString(), uploadResult2.get("url").toString()}; + + Map result = cloudinary.uploader().generateSprite(urls, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + addToDeleteList("sprite", result.get("public_id").toString()); + assertEquals(2, ((Map) result.get("image_infos")).size()); + + result = cloudinary.uploader().generateSprite(sprite_test_tag, asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + addToDeleteList("sprite", result.get("public_id").toString()); + assertEquals(2, ((Map) result.get("image_infos")).size()); + result = cloudinary.uploader().generateSprite(sprite_test_tag, asMap("transformation", "w_100")); + addToDeleteList("sprite", result.get("public_id").toString()); + assertTrue(((String) result.get("css_url")).contains("w_100")); + result = cloudinary.uploader().generateSprite(sprite_test_tag, asMap("transformation", new Transformation().width(100), "format", "jpg")); + addToDeleteList("sprite", result.get("public_id").toString()); + assertTrue(((String) result.get("css_url")).contains("f_jpg,w_100")); + } + + @Test + public void testMulti() throws Exception { + final String MULTI_TEST_TAG = "multi_test_tag" + SUFFIX; + final Map options = asMap("tags", new String[]{MULTI_TEST_TAG, SDK_TEST_TAG, UPLOADER_TAG}); + Map uploadResult1 = cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + Map uploadResult2 = cloudinary.uploader().upload(SRC_TEST_IMAGE, options); + + String[] urls = new String[]{uploadResult1.get("url").toString(), uploadResult2.get("url").toString()}; + + Map result = cloudinary.uploader().multi(urls, asMap("transformation", "c_crop,w_0.5")); + addToDeleteList("multi", result.get("public_id").toString()); + + assertTrue(((String) result.get("url")).endsWith(".gif")); + assertTrue(((String) result.get("url")).contains("w_0.5")); + + List ids = new ArrayList(); + result = cloudinary.uploader().multi(MULTI_TEST_TAG, asMap("transformation", "c_crop,w_0.5")); + addToDeleteList("multi", result.get("public_id").toString()); + Map pdfResult = cloudinary.uploader().multi(MULTI_TEST_TAG, asMap("transformation", new Transformation().width(111), "format", "pdf")); + addToDeleteList("multi", pdfResult.get("public_id").toString()); + + assertTrue(((String) result.get("url")).endsWith(".gif")); + assertTrue(((String) result.get("url")).contains("w_0.5")); + assertTrue(((String) pdfResult.get("url")).contains("w_111")); + assertTrue(((String) pdfResult.get("url")).endsWith(".pdf")); + } + + @Test + public void testTags() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.emptyMap()); + String public_id = (String) result.get("public_id"); + addToDeleteList("upload", public_id); + Map result2 = cloudinary.uploader().upload(SRC_TEST_IMAGE, ObjectUtils.emptyMap()); + String public_id2 = (String) result2.get("public_id"); + addToDeleteList("upload", public_id2); + + //Test add tags + cloudinary.uploader().addTag("tag1", new String[]{public_id, public_id2}, ObjectUtils.emptyMap()); + cloudinary.uploader().addTag("tag2", new String[]{public_id}, ObjectUtils.emptyMap()); + cloudinary.uploader().addTag(new String[]{"tag4","tag5"}, new String[]{public_id}, ObjectUtils.emptyMap()); + List tags = (List) cloudinary.api().resource(public_id, ObjectUtils.emptyMap()).get("tags"); + assertEquals(tags, asArray(new String[]{"tag1", "tag2", "tag4", "tag5"})); + tags = (List) cloudinary.api().resource(public_id2, ObjectUtils.emptyMap()).get("tags"); + assertEquals(tags, asArray(new String[]{"tag1"})); + + //Test remove tags + cloudinary.uploader().removeTag("tag1", new String[]{public_id}, ObjectUtils.emptyMap()); + tags = (List) cloudinary.api().resource(public_id, ObjectUtils.emptyMap()).get("tags"); + assertEquals(tags, asArray(new String[]{"tag2", "tag4", "tag5"})); + cloudinary.uploader().removeTag(new String[]{"tag4", "tag5"}, new String[]{public_id}, ObjectUtils.emptyMap()); + tags = (List) cloudinary.api().resource(public_id, ObjectUtils.emptyMap()).get("tags"); + assertEquals(tags, asArray(new String[]{"tag2"})); + + //Test replace tags + cloudinary.uploader().replaceTag("tag3", new String[]{public_id}, ObjectUtils.emptyMap()); + tags = (List) cloudinary.api().resource(public_id, ObjectUtils.emptyMap()).get("tags"); + assertEquals(tags, asArray(new String[]{"tag3"})); + cloudinary.uploader().replaceTag(new String[]{"tag6", "tag7"}, new String[]{public_id}, ObjectUtils.emptyMap()); + tags = (List) cloudinary.api().resource(public_id, ObjectUtils.emptyMap()).get("tags"); + assertEquals(tags, asArray(new String[]{"tag6", "tag7"})); + + //Test remove all tags + result = cloudinary.uploader().removeAllTags(new String[]{public_id, public_id2, "noSuchId"}, ObjectUtils.emptyMap()); + List publicIds = (List) result.get("public_ids"); + assertThat(publicIds, containsInAnyOrder(public_id, public_id2)); // = and not containing "noSuchId" + result = cloudinary.api().resource(public_id, ObjectUtils.emptyMap()); + assertThat((Map) result, not(hasKey("tags"))); + } + + @Test + public void testAllowedFormats() throws Exception { + //should allow whitelisted formats if allowed_formats + String[] formats = {"png"}; + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("allowed_formats", formats, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals(result.get("format"), "png"); + } + + @Test + public void testAllowedFormatsWithIllegalFormat() throws Exception { + //should prevent non whitelisted formats from being uploaded if allowed_formats is specified + boolean errorFound = false; + String[] formats = {"jpg"}; + try { + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("allowed_formats", formats, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } catch (Exception e) { + errorFound = true; + } + assertTrue(errorFound); + } + + @Test + public void testAllowedFormatsWithFormat() throws Exception { + //should allow non whitelisted formats if type is specified and convert to that type + String[] formats = {"jpg"}; + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("allowed_formats", formats, "format", "jpg", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals("jpg", result.get("format")); + } + + @Test + public void testFaceCoordinates() throws Exception { + //should allow sending face coordinates + Coordinates coordinates = new Coordinates(); + Rectangle rect1 = new Rectangle(121, 31, 110, 51); + Rectangle rect2 = new Rectangle(120, 30, 109, 51); + coordinates.addRect(rect1); + coordinates.addRect(rect2); + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("face_coordinates", coordinates, "faces", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + ArrayList resultFaces = ((ArrayList) result.get("faces")); + assertEquals(2, resultFaces.size()); + + Object[] resultCoordinates = ((ArrayList) resultFaces.get(0)).toArray(); + + assertEquals(rect1.x, resultCoordinates[0]); + assertEquals(rect1.y, resultCoordinates[1]); + assertEquals(rect1.width, resultCoordinates[2]); + assertEquals(rect1.height, resultCoordinates[3]); + + resultCoordinates = ((ArrayList) resultFaces.get(1)).toArray(); + + assertEquals(rect2.x, resultCoordinates[0]); + assertEquals(rect2.y, resultCoordinates[1]); + assertEquals(rect2.width, resultCoordinates[2]); + assertEquals(rect2.height, resultCoordinates[3]); + + Coordinates differentCoordinates = new Coordinates(); + Rectangle rect3 = new Rectangle(122, 32, 111, 152); + differentCoordinates.addRect(rect3); + cloudinary.uploader().explicit((String) result.get("public_id"), asMap("face_coordinates", differentCoordinates, "faces", true, "type", "upload")); + Map info = cloudinary.api().resource((String) result.get("public_id"), asMap("faces", true)); + + resultFaces = (ArrayList) info.get("faces"); + assertEquals(1, resultFaces.size()); + resultCoordinates = ((ArrayList) resultFaces.get(0)).toArray(); + + assertEquals(rect3.x, resultCoordinates[0]); + assertEquals(rect3.y, resultCoordinates[1]); + assertEquals(rect3.width, resultCoordinates[2]); + assertEquals(rect3.height, resultCoordinates[3]); + + } + + @Test + public void testCustomCoordinates() throws Exception { + //should allow sending face coordinates + Coordinates coordinates = new Coordinates("121,31,300,151"); + Map uploadResult = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("custom_coordinates", coordinates, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + Map result = cloudinary.api().resource(uploadResult.get("public_id").toString(), asMap("coordinates", true)); + int[] expected = new int[]{121, 31, SRC_TEST_IMAGE_W, SRC_TEST_IMAGE_H}; + Object[] actual = ((ArrayList) ((ArrayList) ((Map) result.get("coordinates")).get("custom")).get(0)).toArray(); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actual[i]); + } + + coordinates = new Coordinates(new int[]{122, 32, SRC_TEST_IMAGE_W + 100, SRC_TEST_IMAGE_H + 100}); + cloudinary.uploader().explicit((String) uploadResult.get("public_id"), asMap("custom_coordinates", coordinates, "coordinates", true, "type", "upload")); + result = cloudinary.api().resource(uploadResult.get("public_id").toString(), asMap("coordinates", true)); + expected = new int[]{122, 32, SRC_TEST_IMAGE_W + 100, SRC_TEST_IMAGE_H + 100}; + actual = ((ArrayList) ((ArrayList) ((Map) result.get("coordinates")).get("custom")).get(0)).toArray(); + for (int i = 0; i < expected.length; i++) { + assertEquals(expected[i], actual[i]); + } + } + + @Test + public void testModerationRequest() throws Exception { + //should support requesting manual moderation + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("moderation", "manual", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals("manual", ((List) result.get("moderation")).get(0).get("kind")); + assertEquals("pending", ((List) result.get("moderation")).get(0).get("status")); + } + + + @Test + public void testRawConvertRequest() { + //should support requesting raw conversion + try { + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("raw_convert", "illegal", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } catch (Exception e) { + assertTrue(e.getMessage().contains("Raw convert is invalid")); + } + } + + @Test + public void testCategorizationRequest() { + //should support requesting categorization + String errorMessage = ""; + + try { + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("categorization", "illegal", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } catch (Exception e) { + errorMessage = e.getMessage(); + } + + assertTrue(errorMessage.contains("Categorization item illegal is not valid")); + } + + @Test + public void testDetectionRequest() { + //should support requesting detection + String message = null; + try { + cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("detection", "illegal", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + } catch (Exception e) { + message = e.getMessage(); + } + + assertTrue("Detection invalid model 'illegal'".equals(message)); + } + + @Test + public void testUploadLarge() throws Exception { + // support uploading large files + + File temp = File.createTempFile("cldupload.test.", ""); + FileOutputStream out = new FileOutputStream(temp); + int[] header = new int[]{0x42, 0x4D, 0x4A, 0xB9, 0x59, 0x00, 0x00, 0x00, 0x00, 0x00, 0x8A, 0x00, 0x00, 0x00, 0x7C, 0x00, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, 0x78, 0x05, 0x00, 0x00, 0x01, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC0, 0xB8, 0x59, 0x00, 0x61, 0x0F, 0x00, 0x00, 0x61, 0x0F, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x42, 0x47, 0x52, 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x54, 0xB8, 0x1E, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66, 0x66, 0x66, 0xFC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xC4, 0xF5, 0x28, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; + byte[] byteHeader = new byte[138]; + for (int i = 0; i <= 137; i++) byteHeader[i] = (byte) header[i]; + byte[] piece = new byte[10]; + Arrays.fill(piece, (byte) 0xff); + out.write(byteHeader); + for (int i = 1; i <= 588000; i++) { + out.write(piece); + } + out.close(); + assertEquals(5880138, temp.length()); + + String[] tags = new String[]{"upload_large_tag_" + SUFFIX, SDK_TEST_TAG, UPLOADER_TAG}; + + Map resource = cloudinary.uploader().uploadLarge(temp, asMap("use_filename", true, "resource_type", "raw", "chunk_size", 5243000, "tags", tags)); + assertArrayEquals(tags, ((java.util.ArrayList) resource.get("tags")).toArray()); + + assertEquals("raw", resource.get("resource_type")); + assertTrue(resource.get("public_id").toString().startsWith("cldupload")); + + resource = cloudinary.uploader().uploadLarge(new FileInputStream(temp), asMap("filename", "test123", "chunk_size", 5243000, "tags", tags)); + assertArrayEquals(tags, ((java.util.ArrayList) resource.get("tags")).toArray()); + assertEquals("image", resource.get("resource_type")); + assertEquals(1400, resource.get("width")); + assertEquals(1400, resource.get("height")); + assertEquals("test123", resource.get("original_filename")); + + resource = cloudinary.uploader().uploadLarge(temp, asMap("chunk_size", 5880138, "tags", tags)); + assertArrayEquals(tags, ((java.util.ArrayList) resource.get("tags")).toArray()); + assertEquals("image", resource.get("resource_type")); + assertEquals(1400, resource.get("width")); + assertEquals(1400, resource.get("height")); + + resource = cloudinary.uploader().uploadLarge(new FileInputStream(temp), asMap("chunk_size", 5880138, "tags", tags)); + assertArrayEquals(tags, ((java.util.ArrayList) resource.get("tags")).toArray()); + assertEquals("image", resource.get("resource_type")); + assertEquals(1400, resource.get("width")); + assertEquals(1400, resource.get("height")); + } + + @Test + public void testUnsignedUpload() throws Exception { + // should support unsigned uploading using presets + Map preset = cloudinary.api().createUploadPreset(asMap("folder", "upload_folder", "unsigned", true)); + Map result = cloudinary.uploader().unsignedUpload(SRC_TEST_IMAGE, preset.get("name").toString(), asMap("tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertTrue(result.get("public_id").toString().matches("^upload_folder\\/[a-z0-9]+$")); + cloudinary.api().deleteUploadPreset(preset.get("name").toString(), ObjectUtils.emptyMap()); + } + + @Test + public void testFilenameOption() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("filename", "emanelif", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals("emanelif", result.get("original_filename")); + } + + + @Test + public void testFilenameOverrideOption() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("filename_override", "overridden", "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertEquals("overridden", result.get("original_filename")); + } + + + @Test + public void testResponsiveBreakpoints() throws Exception { + ResponsiveBreakpoint breakpoint = new ResponsiveBreakpoint() + .createDerived(true) + .maxImages(2) + .transformation(new Transformation().angle(90)) + .format("gif"); + + // A single breakpoint + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("responsive_breakpoints", + breakpoint, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + + java.util.ArrayList breakpointsResponse = (java.util.ArrayList) result.get("responsive_breakpoints"); + Map map = (Map) breakpointsResponse.get(0); + + java.util.ArrayList breakpoints = (java.util.ArrayList) map.get("breakpoints"); + assertTrue(((Map) breakpoints.get(0)).get("url").toString().endsWith("gif")); + assertEquals("a_90", map.get("transformation")); + + // check again with transformation + format + breakpoint.transformation(new Transformation().effect("sepia")); + + // an array of breakpoints + result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("responsive_breakpoints", + new ResponsiveBreakpoint[]{breakpoint}, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG) + )); + breakpointsResponse = (java.util.ArrayList) result.get("responsive_breakpoints"); + breakpoints = (java.util.ArrayList) ((Map) breakpointsResponse.get(0)).get("breakpoints"); + assertEquals(2, breakpoints.size()); + assertTrue(((Map) breakpoints.get(0)).get("url").toString().endsWith("gif")); + + // a JSONArray of breakpoints + JSONArray array = new JSONArray(); + array.put(breakpoint); + result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("responsive_breakpoints", array, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG) + )); + breakpointsResponse = (java.util.ArrayList) result.get("responsive_breakpoints"); + breakpoints = (java.util.ArrayList) ((Map) breakpointsResponse.get(0)).get("breakpoints"); + assertEquals(2, breakpoints.size()); + } + + @Test + public void testCreateArchive() throws Exception { + List toDelete = new ArrayList(2); + Map result = cloudinary.uploader().createArchive(new ArchiveParams().tags(new String[]{ARCHIVE_TAG})); + toDelete.add(result.get("public_id").toString()); + assertEquals(2, result.get("file_count")); + result = cloudinary.uploader().createArchive( + new ArchiveParams().tags(new String[]{ARCHIVE_TAG}).transformations( + new Transformation[]{new Transformation().width(0.5), new Transformation().width(2.0)})); + toDelete.add(result.get("public_id").toString()); + + assertEquals(4, result.get("file_count")); + cloudinary.api().deleteResources(toDelete, asMap("resource_type", "raw")); + } + + + @Test + public void testCreateArchiveRaw() throws Exception { + Map result = cloudinary.uploader().createArchive(new ArchiveParams().tags(new String[]{ARCHIVE_TAG}).resourceType("raw")); + assertEquals(1, result.get("file_count")); + cloudinary.api().deleteResources(Arrays.asList(result.get("public_id").toString()), asMap("resource_type", "raw")); + + } + + @Test + public void testCreateZipMultipleResourceTypes() throws Exception { + Map result = cloudinary.uploader().createZip(ObjectUtils.asMap("fully_qualified_public_ids",(new String[]{SRC_FULLY_QUALIFIED_IMAGE,SRC_FULLY_QUALIFIED_VIDEO}),"resource_type","auto")); + assertEquals(2, result.get("file_count")); + cloudinary.api().deleteResources(Arrays.asList(result.get("public_id").toString()), asMap("resource_type", "raw")); + } + + @Test + public void testDownloadArchive() throws Exception { + String result = cloudinary.downloadArchive(new ArchiveParams().tags(new String[]{ARCHIVE_TAG}).targetTags(new String[]{UPLOADER_TAG})); + URL url = new java.net.URL(result); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + ZipInputStream in = new ZipInputStream(new BufferedInputStream(urlConnection.getInputStream())); + int files = 0; + try { + while ((in.getNextEntry()) != null) { + files += 1; + } + } finally { + in.close(); + } + assertEquals(2, files); + } + + public void testUploadInvalidUrl() { + try { + cloudinary.uploader().upload(REMOTE_TEST_IMAGE + "\n", asMap("return_error", true)); + fail("Expected exception was not thrown"); + } catch (IOException e) { + assertEquals(e.getMessage(), "File not found or unreadable: " + REMOTE_TEST_IMAGE + "\n"); + } + } + + @Test + public void testAccessControl() throws ParseException, IOException { + SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); + final Date start = simpleDateFormat.parse("2019-02-22 16:20:57 +0200"); + final Date end = simpleDateFormat.parse("2019-03-22 00:00:00 +0200"); + AccessControlRule acl; + AccessControlRule token = AccessControlRule.token(); + + acl = AccessControlRule.anonymous(start, null); + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("access_control", + Arrays.asList(acl, token), "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + + assertNotNull(result); + List> accessControlResponse = (List>) result.get("access_control"); + assertNotNull(accessControlResponse); + assertEquals(2, accessControlResponse.size()); + + Map acr = accessControlResponse.get(0); + assertEquals("anonymous", acr.get("access_type")); + assertEquals("2019-02-22T14:20:57Z", acr.get("start")); + assertThat(acr, not(hasKey("end"))); + + acr = accessControlResponse.get(1); + assertEquals("token", acr.get("access_type")); + assertThat(acr, not(hasKey("start"))); + assertThat(acr, not(hasKey("end"))); + + result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("access_control", + acl, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + + assertNotNull(result); + accessControlResponse = (List>) result.get("access_control"); + assertNotNull(accessControlResponse); + acr = accessControlResponse.get(0); + assertEquals(1, accessControlResponse.size()); + assertEquals("anonymous", acr.get("access_type")); + assertEquals("2019-02-22T14:20:57Z", acr.get("start")); + assertThat(acr, not(hasKey("end"))); + + String aclString = "[{\"access_type\":\"anonymous\",\"start\":\"2019-02-22 16:20:57 +0200\",\"end\":\"2019-03-22 00:00 +0200\"}]"; + result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("access_control", + aclString, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + + assertNotNull(result); + accessControlResponse = (List>) result.get("access_control"); + assertNotNull(accessControlResponse); + assertTrue(accessControlResponse.size() == 1); + assertEquals("anonymous", accessControlResponse.get(0).get("access_type")); + assertEquals("2019-02-22T14:20:57Z", accessControlResponse.get(0).get("start")); + assertEquals("2019-03-21T22:00:00Z", accessControlResponse.get(0).get("end")); + } + + @Test + public void testOnSuccessScript() throws Exception { + String tags = "[\"autocaption\"" + ",\"" + SDK_TEST_TAG + "\",\"" + UPLOADER_TAG + "\"]"; + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("on_success", "current_asset.update({tags:" + tags + "});")); + assertTrue(((List)result.get("tags")).contains("autocaption")); + } + + @Test + public void testQualityAnalysis() throws IOException { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("quality_analysis", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertNotNull(result.get("quality_analysis")); + result = cloudinary.uploader().explicit(result.get("public_id").toString(), ObjectUtils.asMap("type", "upload", "resource_type", "image", "quality_analysis", true)); + assertNotNull(result.get("quality_analysis")); + + } + + @Test + public void testCinemagraphAnalysisUpload() throws IOException { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("cinemagraph_analysis", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertNotNull(result.get("cinemagraph_analysis")); + result = cloudinary.uploader().explicit(result.get("public_id").toString(), ObjectUtils.asMap("type", "upload", "resource_type", "image", "cinemagraph_analysis", true)); + assertNotNull(result.get("cinemagraph_analysis")); + + } + + @Test + public void testAccessibilityAnalysisUpload() throws IOException { + Map result = cloudinary.uploader().upload(SRC_TEST_IMAGE, asMap("accessibility_analysis", true, "tags", Arrays.asList(SDK_TEST_TAG, UPLOADER_TAG))); + assertNotNull(result.get("accessibility_analysis")); + result = cloudinary.uploader().explicit(result.get("public_id").toString(), ObjectUtils.asMap("type", "upload", "resource_type", "image", "accessibility_analysis", true)); + assertNotNull(result.get("accessibility_analysis")); + } + + private void addToDeleteList(String type, String id) { + Set ids = toDelete.get(type); + if (ids == null) { + ids = new HashSet(); + toDelete.put(type, ids); + } + + ids.add(id); + } + + @Test + public void testUploadLocalUnicodeFilename() throws Exception { + Map result = cloudinary.uploader().upload(HEBREW_PDF, asMap("resource_type", "raw")); + assertTrue(((String)result.get("public_id")).contains(".docx")); + } + + @Test + public void testUploadFolderDecoupling() { + //TODO: Need to build a unit testing infrastructure + Map options = asMap( + "use_filename_as_display_name", true, + "public_id_prefix", "test_id_prefix", + "asset_folder", "asset_folder_test", + "display_name", "display_name_test", + "use_asset_folder_as_public_id_prefix", true, + "visual_search", true); + + Map uploadParams = Util.buildUploadParams(options); + Assert.assertEquals("test_id_prefix", uploadParams.get("public_id_prefix")); + Assert.assertEquals(true, uploadParams.get("use_filename_as_display_name")); + Assert.assertEquals("asset_folder_test", uploadParams.get("asset_folder")); + Assert.assertEquals("display_name_test", uploadParams.get("display_name")); + Assert.assertEquals(true, uploadParams.get("use_asset_folder_as_public_id_prefix")); + Assert.assertEquals(true, uploadParams.get("visual_search")); + } + + @Test + public void testNotificationUrl() { + Map options = asMap("notification_url", "https://www.test.com"); + Map uploadParams = Util.buildUploadParams(options); + Assert.assertEquals("https://www.test.com", uploadParams.get("notification_url")); + } + + @Test + public void testAutoChaptering() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_VIDEO, asMap( + "resource_type", "video", "auto_chaptering", true)); + assert(result != null); + assertNotNull(result.get("playback_url")); + } + + @Test + public void testAutoTranscription() throws Exception { + Map result = cloudinary.uploader().upload(SRC_TEST_VIDEO, asMap( + "resource_type", "video", "auto_transcription", true)); + assert(result != null); + assertNotNull(result.get("playback_url")); + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/MetadataTestHelper.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/MetadataTestHelper.java new file mode 100644 index 00000000..2a128c7f --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/MetadataTestHelper.java @@ -0,0 +1,26 @@ +package com.cloudinary.test; + +import com.cloudinary.Api; +import com.cloudinary.api.ApiResponse; +import com.cloudinary.metadata.MetadataField; +import com.cloudinary.metadata.MetadataValidation; +import com.cloudinary.metadata.StringMetadataField; + +public final class MetadataTestHelper { + private MetadataTestHelper() {} + + public static StringMetadataField newFieldInstance(String label, Boolean mandatory) throws Exception { + StringMetadataField field = new StringMetadataField(); + field.setLabel(label); + field.setMandatory(mandatory); + field.setValidation(new MetadataValidation.StringLength(3, 9)); + field.setDefaultValue("val_test"); + return field; + } + + public static ApiResponse addFieldToAccount(Api api, MetadataField field) throws Exception { + ApiResponse apiResponse = api.addMetadataField(field); + return apiResponse; + } +} + diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/MockableTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/MockableTest.java new file mode 100644 index 00000000..92b272dd --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/MockableTest.java @@ -0,0 +1,78 @@ +package com.cloudinary.test; + +import com.cloudinary.Cloudinary; +import com.cloudinary.test.helpers.Feature; +import com.cloudinary.utils.ObjectUtils; +import com.cloudinary.utils.StringUtils; + +import static org.junit.Assume.assumeTrue; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class MockableTest { + + public static final String HEBREW_PDF = "../cloudinary-test-common/src/main/resources/אבג.docx"; + public static final String SRC_TEST_IMAGE = "../cloudinary-test-common/src/main/resources/old_logo.png"; + public static final String SRC_TEST_VIDEO = "http://res.cloudinary.com/demo/video/upload/dog.mp4"; + public static final String SRC_TEST_RAW = "../cloudinary-test-common/src/main/resources/docx.docx"; + public static final String REMOTE_TEST_IMAGE = "http://cloudinary.com/images/old_logo.png"; + protected static String SUFFIX = StringUtils.isNotBlank(System.getenv("TRAVIS_JOB_ID")) ? System.getenv("TRAVIS_JOB_ID") : String.valueOf(new Random().nextInt(99999)); + protected static final String SDK_TEST_TAG = "cloudinary_java_test_" + SUFFIX; + protected Cloudinary cloudinary; + + protected Object getParam(String name){ + throw new UnsupportedOperationException(); + } + protected String getURL(){ + throw new UnsupportedOperationException(); + } + protected String getHttpMethod(){ + throw new UnsupportedOperationException(); + } + + protected Map preloadResource(Map options) throws IOException { + if (!options.containsKey("tags")){ + throw new IllegalArgumentException("Must provide unique per-class tags"); + } + Map combinedOptions = ObjectUtils.asMap("transformation", "c_scale,w_100"); + combinedOptions.putAll(options); + return cloudinary.uploader().upload("http://res.cloudinary.com/demo/image/upload/sample", combinedOptions); + } + + private static final List enabledAddons = getEnabledAddons(); + + protected void assumeAddonEnabled(String addon) throws Exception { + boolean enabled = enabledAddons.contains(addon.toLowerCase()) + || (enabledAddons.size() == 1 && enabledAddons.get(0).equalsIgnoreCase("all")); + + assumeTrue(String.format("Use CLD_TEST_ADDONS environment variable to enable tests for %s.", addon), enabled); + } + + private static List getEnabledAddons() { + String envAddons = System.getenv() + .getOrDefault("CLD_TEST_ADDONS", "") + .toLowerCase() + .replaceAll("\\s", ""); + + return Arrays.asList(envAddons.split(",")); + } + + protected static boolean shouldTestFeature(String feature) { + String sdkFeatures = System.getenv() + .getOrDefault("CLD_TEST_FEATURES", "") + .toLowerCase() + .replaceAll("\\s", ""); + List sdkFeaturesList = Arrays.asList(sdkFeatures.split(",")); + return sdkFeatures.contains(feature.toLowerCase()) || (sdkFeaturesList.size() == 1 && sdkFeaturesList.get(0).equalsIgnoreCase(Feature.ALL)); + } + + static protected boolean assumeCloudinaryAccountURLExist() { + String cloudinaryAccountUrl = System.getProperty("CLOUDINARY_ACCOUNT_URL", System.getenv("CLOUDINARY_ACCOUNT_URL")); + assumeTrue(String.format("Use CLOUDINARY_ACCOUNT_URL environment variable to enable tests"), cloudinaryAccountUrl != null); + return cloudinaryAccountUrl != null; + } +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/TimeoutTest.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/TimeoutTest.java new file mode 100644 index 00000000..ab67bd48 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/TimeoutTest.java @@ -0,0 +1,7 @@ +package com.cloudinary.test; + +/*** + * Marker interface for Junit categories. + */ +public interface TimeoutTest { +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/helpers/Feature.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/helpers/Feature.java new file mode 100644 index 00000000..b66bd303 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/helpers/Feature.java @@ -0,0 +1,10 @@ +package com.cloudinary.test.helpers; + +public final class Feature { + private Feature() {} + + public static final String ALL = "all"; + public static final String DYNAMIC_FOLDERS = "dynamic_folders"; + public static final String BACKEDUP_ASSETS = "backedup_assets"; + public static final String CONDITIONAL_METADATA_RULES = "conditional_metadata_rules"; +} diff --git a/cloudinary-test-common/src/main/java/com/cloudinary/test/rules/RetryRule.java b/cloudinary-test-common/src/main/java/com/cloudinary/test/rules/RetryRule.java new file mode 100644 index 00000000..4d407610 --- /dev/null +++ b/cloudinary-test-common/src/main/java/com/cloudinary/test/rules/RetryRule.java @@ -0,0 +1,47 @@ +package com.cloudinary.test.rules; + +import org.junit.rules.TestRule; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; + +import java.util.Objects; + +public class RetryRule implements TestRule { + private int retryCount; + private int delay; + + public RetryRule(int retryCount, int delay) { + this.retryCount = retryCount; + this.delay = delay; + } + + public RetryRule() { + this.retryCount = 3; + this.delay = 3; + } + + public Statement apply(Statement base, Description description) { + return statement(base, description); + } + + private Statement statement(final Statement base, final Description description) { + return new Statement() { + @Override + public void evaluate() throws Throwable { + Throwable caughtThrowable = null; + for (int i = 0; i < retryCount; i++) { + try { + base.evaluate(); + return; + } catch (Throwable t) { + caughtThrowable = t; + System.err.println(description.getDisplayName() + ": run " + (i + 1) + " failed."); + Thread.sleep(delay * 1000); + } + } + System.err.println(description.getDisplayName() + ": Giving up after " + retryCount + " failures."); + throw Objects.requireNonNull(caughtThrowable); + } + }; + } +} diff --git a/cloudinary-core/src/test/resources/docx.docx b/cloudinary-test-common/src/main/resources/docx.docx similarity index 100% rename from cloudinary-core/src/test/resources/docx.docx rename to cloudinary-test-common/src/main/resources/docx.docx diff --git a/cloudinary-core/src/test/resources/favicon.ico b/cloudinary-test-common/src/main/resources/favicon.ico similarity index 100% rename from cloudinary-core/src/test/resources/favicon.ico rename to cloudinary-test-common/src/main/resources/favicon.ico diff --git a/cloudinary-core/src/test/resources/logo.png b/cloudinary-test-common/src/main/resources/old_logo.png similarity index 100% rename from cloudinary-core/src/test/resources/logo.png rename to cloudinary-test-common/src/main/resources/old_logo.png diff --git "a/cloudinary-test-common/src/main/resources/\327\220\327\221\327\222.docx" "b/cloudinary-test-common/src/main/resources/\327\220\327\221\327\222.docx" new file mode 100644 index 00000000..2022c4ca Binary files /dev/null and "b/cloudinary-test-common/src/main/resources/\327\220\327\221\327\222.docx" differ diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..2fc928d3 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,22 @@ +publishRepo=https://central.sonatype.com/ +snapshotRepo=https://central.sonatype.com/ +publishDescription=Cloudinary is a cloud service that offers a solution to a web application's entire image management pipeline. Upload images to the cloud. Automatically perform smart image resizing, cropping and conversion without installing any complex software. Integrate Facebook or Twitter profile image extraction in a snap, in any dimension and style to match your website’s graphics requirements. Images are seamlessly delivered through a fast CDN, and much much more. This Java library allows to easily integrate with Cloudinary in Java applications. +githubUrl=http://github.com/cloudinary/cloudinary_java +scmConnection=scm:git:git://github.com/cloudinary/cloudinary_java.git +scmDeveloperConnection=scm:git:git@github.com:cloudinary/cloudinary_java.git' +scmUrl=http://github.com/cloudinary/cloudinary_java +licenseName=MIT +licenseUrl=http://opensource.org/licenses/MIT +developerId=cloudinary +developerName=Cloudinary +developerEmail=info@cloudinary.com + +# These two properties must use these exact names to be compatible with 'gradle install' plugin. +group=com.cloudinary +version=2.3.2 + +gnsp.disableApplyOnlyOnRootProjectEnforcement=true + +# see https://github.com/gradle/gradle/issues/11308 +systemProp.org.gradle.internal.publish.checksums.insecure=true + diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..758de960 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..30b572c7 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# 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 +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="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# 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 + ;; +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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +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. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +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 +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 + +# 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\"" + fi + i=$((i+1)) + 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")" +fi + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f9553162 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@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= + +@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 diff --git a/java_shared.gradle b/java_shared.gradle new file mode 100644 index 00000000..f7e6e550 --- /dev/null +++ b/java_shared.gradle @@ -0,0 +1,41 @@ +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +javadoc { + options.encoding = 'UTF-8' +} + +test { + testLogging.showStandardStreams = true + testLogging.exceptionFormat = 'full' +} + +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = 'sources' + from sourceSets.main.allSource +} + +task javadocJar(type: Jar, dependsOn: javadoc) { + classifier = 'javadoc' + from javadoc.destinationDir +} + +artifacts { + archives javadocJar, sourcesJar +} + +tasks.withType(GenerateModuleMetadata) { + enabled = false +} + +tasks.withType(Test) { + environment 'CLOUDINARY_URL', System.getProperty('CLOUDINARY_URL') + maxParallelForks = Runtime.runtime.availableProcessors() + + // show standard out and standard error of the test JVM(s) on the console + testLogging.showStandardStreams = true +} + +tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' +} diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 36c12ce3..00000000 --- a/pom.xml +++ /dev/null @@ -1,186 +0,0 @@ - - 4.0.0 - - - org.sonatype.oss - oss-parent - 7 - - - com.cloudinary - cloudinary-parent - 1.0.15-SNAPSHOT - pom - Cloudinary Java Client Library Parent Project - - - cloudinary-core - cloudinary-taglib - - - - Cloudinary is a cloud service that offers a solution to a web application's entire image management pipeline. - Easily upload images to the cloud. Automatically perform smart image resizing, cropping and conversion without installing any complex software. - Integrate Facebook or Twitter profile image extraction in a snap, in any dimension and style to match your website’s graphics requirements. - Images are seamlessly delivered through a fast CDN, and much much more. This Java library allows to easily integrate with Cloudinary in Java applications. - - http://github.com/cloudinary/cloudinary_java - - - - MIT - http://opensource.org/licenses/MIT - repo - - - - - - cloudinary - Cloudinary - info@cloudinary.com - - - - - scm:git:git://github.com/cloudinary/cloudinary_java.git - scm:git:git@github.com:cloudinary/cloudinary_java.git - http://github.com/cloudinary/cloudinary_java - - - - UTF-8 - UTF-8 - - - - - - maven-compiler-plugin - 3.0 - - 1.6 - 1.6 - UTF-8 - - - - org.apache.maven.plugins - maven-source-plugin - 2.2.1 - - - attach-sources - - jar - - - - - - org.apache.maven.plugins - maven-javadoc-plugin - 2.9 - - - attach-javadocs - - jar - - - - - - - - - - commons-lang - commons-lang - 2.6 - - - commons-codec - commons-codec - 1.6 - - - commons-collections - commons-collections - 3.2.1 - - - commons-logging - commons-logging - 1.1.1 - - - org.apache.httpcomponents - httpclient - 4.2.1 - - - org.apache.httpcomponents - httpmime - 4.2.1 - - - org.apache.httpcomponents - httpcore - 4.2.1 - - - com.googlecode.json-simple - json-simple - 1.1.1 - - - junit - junit - 4.10 - test - - - - - - release-sign-artifacts - - - performRelease - true - - - - - - org.apache.maven.plugins - maven-gpg-plugin - - - sign-artifacts - verify - - sign - - - - - - - - - local - - - snapshots - http://localhost:8081/nexus/content/repositories/snapshots - - - releases - http://localhost:8081/nexus/content/repositories/releases - - - - - diff --git a/publish.gradle b/publish.gradle new file mode 100644 index 00000000..80eb3ea9 --- /dev/null +++ b/publish.gradle @@ -0,0 +1,73 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' + +// Simple module-level publishing for manual upload to Central Portal +if (hasProperty("ossrhTokenPassword") || hasProperty("centralPassword")) { + + publishing { + publications { + mavenJava(MavenPublication) { + // Set coordinates from gradle.properties + groupId = project.ext.publishGroupId + artifactId = project.name + version = project.version + + // Include JAR artifacts and components for Java + from components.java + artifact sourcesJar + artifact javadocJar + + pom { + name = getModuleName(project.name) + packaging = 'jar' + description = publishDescription + url = githubUrl + + licenses { + license { + name = licenseName + url = licenseUrl + } + } + + developers { + developer { + id = developerId + name = developerName + email = developerEmail + } + } + + scm { + connection = scmConnection + developerConnection = scmDeveloperConnection + url = scmUrl + } + } + } + } + } + + // Signing temporarily disabled - we'll add GPG signatures manually using command line + // signing { + // required { project.hasProperty("centralPassword") } + // useGpgCmd() + // sign publishing.publications.mavenJava + // } +} + +// Helper function to get proper module names +def getModuleName(artifactId) { + switch(artifactId) { + case 'cloudinary-core': + return 'Cloudinary Core Library' + case 'cloudinary-http5': + return 'Cloudinary Apache HTTP 5 Library' + case 'cloudinary-taglib': + return 'Cloudinary Taglib Library' + case 'cloudinary-test-common': + return 'Cloudinary Test Common Library' + default: + return 'Cloudinary Java Library' + } +} \ No newline at end of file diff --git a/samples/photo_album/pom.xml b/samples/photo_album/pom.xml index 6e523c7c..e30bf14b 100644 --- a/samples/photo_album/pom.xml +++ b/samples/photo_album/pom.xml @@ -8,7 +8,7 @@ photo_album - 3.2.0.RELEASE + 5.3.18 @@ -23,7 +23,12 @@ com.cloudinary cloudinary-taglib - 1.0.14 + 1.14.0 + + + com.cloudinary + cloudinary-http44 + 1.14.0 org.springframework @@ -66,7 +71,7 @@ junit junit - 4.8.2 + 4.12 test @@ -79,43 +84,43 @@ org.springframework.data spring-data-jpa - 1.3.0.RELEASE + 1.11.20.RELEASE org.hibernate.javax.persistence hibernate-jpa-2.0-api - 1.0.0.Final + 1.0.1.Final org.hibernate hibernate-entitymanager - 3.6.10.Final + 5.2.10.Final org.hsqldb hsqldb - 2.2.9 + 2.7.1 commons-fileupload commons-fileupload - 1.3 + 1.3.3 javax.validation validation-api - 1.1.0.Final + 2.0.0.Final org.hibernate - hibernate-validator-annotation-processor - 4.1.0.Final + hibernate-validator-annotation-processor + 6.0.1.Final diff --git a/samples/photo_album/src/main/java/cloudinary/controllers/PhotoController.java b/samples/photo_album/src/main/java/cloudinary/controllers/PhotoController.java index 0da6ad84..0f44739b 100644 --- a/samples/photo_album/src/main/java/cloudinary/controllers/PhotoController.java +++ b/samples/photo_album/src/main/java/cloudinary/controllers/PhotoController.java @@ -6,6 +6,7 @@ import cloudinary.repositories.PhotoRepository; import com.cloudinary.Cloudinary; +import com.cloudinary.utils.ObjectUtils; import com.cloudinary.Singleton; import org.springframework.beans.factory.annotation.Autowired; @@ -38,9 +39,15 @@ public String uploadPhoto(@ModelAttribute PhotoUpload photoUpload, BindingResult Map uploadResult = null; if (photoUpload.getFile() != null && !photoUpload.getFile().isEmpty()) { uploadResult = Singleton.getCloudinary().uploader().upload(photoUpload.getFile().getBytes(), - Cloudinary.asMap("resource_type", "auto")); + ObjectUtils.asMap("resource_type", "auto")); photoUpload.setPublicId((String) uploadResult.get("public_id")); - photoUpload.setVersion((Long) uploadResult.get("version")); + Object version = uploadResult.get("version"); + if (version instanceof Integer) { + photoUpload.setVersion(new Long((Integer) version)); + } else { + photoUpload.setVersion((Long) version); + } + photoUpload.setSignature((String) uploadResult.get("signature")); photoUpload.setFormat((String) uploadResult.get("format")); photoUpload.setResourceType((String) uploadResult.get("resource_type")); @@ -79,10 +86,10 @@ public String directUnsignedUploadPhotoForm(ModelMap model) throws Exception { model.addAttribute("photoUpload", new PhotoUpload()); model.addAttribute("unsigned", true); Cloudinary cld = Singleton.getCloudinary(); - String preset = "sample_" + cld.apiSignRequest(Cloudinary.asMap("api_key", cld.getStringConfig("api_key")), cld.getStringConfig("api_secret")).substring(0, 10); + String preset = "sample_" + cld.apiSignRequest(ObjectUtils.asMap("api_key", cld.config.apiKey), cld.config.apiSecret).substring(0, 10); model.addAttribute("preset", preset); try { - Singleton.getCloudinary().api().createUploadPreset(Cloudinary.asMap( + Singleton.getCloudinary().api().createUploadPreset(ObjectUtils.asMap( "name", preset, "unsigned", true, "folder", "preset_folder")); @@ -90,4 +97,4 @@ public String directUnsignedUploadPhotoForm(ModelMap model) throws Exception { } return "direct_upload_form"; } -} \ No newline at end of file +} diff --git a/samples/photo_album/src/main/java/cloudinary/lib/PhotoUploadValidator.java b/samples/photo_album/src/main/java/cloudinary/lib/PhotoUploadValidator.java index 2bcdc56d..0a389e02 100644 --- a/samples/photo_album/src/main/java/cloudinary/lib/PhotoUploadValidator.java +++ b/samples/photo_album/src/main/java/cloudinary/lib/PhotoUploadValidator.java @@ -1,16 +1,10 @@ package cloudinary.lib; import cloudinary.models.PhotoUpload; -import com.cloudinary.Cloudinary; -import com.cloudinary.Singleton; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - public class PhotoUploadValidator implements Validator { public boolean supports(Class clazz) { return PhotoUpload.class.equals(clazz); diff --git a/samples/photo_album/src/main/resources/META-INF/persistence.xml b/samples/photo_album/src/main/resources/META-INF/persistence.xml index 898749ed..8a7baf27 100644 --- a/samples/photo_album/src/main/resources/META-INF/persistence.xml +++ b/samples/photo_album/src/main/resources/META-INF/persistence.xml @@ -1,7 +1,7 @@ - org.hibernate.ejb.HibernatePersistence + org.hibernate.jpa.HibernatePersistenceProvider diff --git a/samples/photo_album/src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml b/samples/photo_album/src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml index 6209a18f..7922432e 100644 --- a/samples/photo_album/src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml +++ b/samples/photo_album/src/main/webapp/WEB-INF/mvc-dispatcher-servlet.xml @@ -31,7 +31,7 @@ class="org.springframework.web.multipart.commons.CommonsMultipartResolver"> - + - " target="_blank">Non Image File + + " target="_blank">Open in new Tab + + + + " target="_blank">Non Image File + diff --git a/samples/photo_album/src/main/webapp/WEB-INF/pages/pre.jsp b/samples/photo_album/src/main/webapp/WEB-INF/pages/pre.jsp index 8b78072f..c219fce2 100644 --- a/samples/photo_album/src/main/webapp/WEB-INF/pages/pre.jsp +++ b/samples/photo_album/src/main/webapp/WEB-INF/pages/pre.jsp @@ -12,7 +12,7 @@
\ No newline at end of file diff --git a/samples/photo_album_gae/pom.xml b/samples/photo_album_gae/pom.xml index bfdbe2ef..9be3ebf6 100644 --- a/samples/photo_album_gae/pom.xml +++ b/samples/photo_album_gae/pom.xml @@ -8,9 +8,9 @@ photo_album_gae - 3.2.0.RELEASE + 5.3.19 1 - 1.8.9 + 1.9.37 UTF-8 @@ -38,7 +38,12 @@ com.cloudinary cloudinary-taglib - 1.0.13 + 1.4.1 + + + com.cloudinary + cloudinary-http5 + 2.0.0 org.springframework @@ -81,7 +86,7 @@ junit junit - 4.10 + 4.12 test @@ -94,7 +99,7 @@ org.springframework.data spring-data-jpa - 1.2.0.RELEASE + 1.11.20.RELEASE @@ -106,7 +111,7 @@ commons-fileupload commons-fileupload - 1.3 + 1.3.3 @@ -121,7 +126,7 @@ 1.9.0 test - + com.google.appengine appengine-testing @@ -140,7 +145,7 @@ hibernate-validator-annotation-processor 4.1.0.Final - + gmultipart gmultipart diff --git a/samples/photo_album_gae/src/main/java/cloudinary/controllers/PhotoController.java b/samples/photo_album_gae/src/main/java/cloudinary/controllers/PhotoController.java index fd71cb6f..7a6438a8 100644 --- a/samples/photo_album_gae/src/main/java/cloudinary/controllers/PhotoController.java +++ b/samples/photo_album_gae/src/main/java/cloudinary/controllers/PhotoController.java @@ -2,7 +2,7 @@ import cloudinary.lib.PhotoUploadValidator; import cloudinary.models.PhotoUpload; -import com.cloudinary.Cloudinary; +import com.cloudinary.utils.ObjectUtils; import com.cloudinary.Singleton; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -27,7 +27,8 @@ @Controller @RequestMapping("/") public class PhotoController { - private final static GAEConnectionManager connectoinManager = new GAEConnectionManager(); + + private final static GAEConnectionManager connectionManager = new GAEConnectionManager(); @RequestMapping(value = "/", method = RequestMethod.GET) public String listPhotos(ModelMap model) { DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); @@ -47,12 +48,13 @@ public String uploadPhoto(@ModelAttribute PhotoUpload photoUpload, BindingResult validator.validate(photoUpload, result); Map uploadResult = null; - if (photoUpload.getFile() != null && !photoUpload.getFile().isEmpty()) { - uploadResult = Singleton.getCloudinary().uploader().withConnectionManager(connectoinManager).upload(photoUpload.getFile().getBytes(), - Cloudinary.asMap("resource_type", "auto")); + if (photoUpload.getFile() != null && !photoUpload.getFile().isEmpty()) { + Singleton.getCloudinary().config.properties.put("connectionManager", connectionManager); + uploadResult = Singleton.getCloudinary().uploader().upload(photoUpload.getFile().getBytes(), + ObjectUtils.asMap("resource_type", "auto")); photoUpload.setPublicId((String) uploadResult.get("public_id")); - photoUpload.setVersion((Long) uploadResult.get("version")); + photoUpload.setVersion(((Integer) uploadResult.get("version")).longValue()); photoUpload.setSignature((String) uploadResult.get("signature")); photoUpload.setFormat((String) uploadResult.get("format")); photoUpload.setResourceType((String) uploadResult.get("resource_type")); @@ -84,4 +86,4 @@ public String directUploadPhotoForm(ModelMap model) { model.addAttribute("photo", new PhotoUpload()); return "direct_upload_form"; } -} \ No newline at end of file +} diff --git a/samples/photo_album_gae/src/main/java/cloudinary/lib/PhotoUploadValidator.java b/samples/photo_album_gae/src/main/java/cloudinary/lib/PhotoUploadValidator.java index 2bcdc56d..0a389e02 100644 --- a/samples/photo_album_gae/src/main/java/cloudinary/lib/PhotoUploadValidator.java +++ b/samples/photo_album_gae/src/main/java/cloudinary/lib/PhotoUploadValidator.java @@ -1,16 +1,10 @@ package cloudinary.lib; import cloudinary.models.PhotoUpload; -import com.cloudinary.Cloudinary; -import com.cloudinary.Singleton; import org.springframework.validation.Errors; import org.springframework.validation.ValidationUtils; import org.springframework.validation.Validator; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - public class PhotoUploadValidator implements Validator { public boolean supports(Class clazz) { return PhotoUpload.class.equals(clazz); diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..004842ea --- /dev/null +++ b/settings.gradle @@ -0,0 +1,6 @@ +rootProject.name = 'cloudinary-parent' +include ':cloudinary-core' +include ':cloudinary-taglib' +include ':cloudinary-http5' +include ':cloudinary-test-common' + diff --git a/tools/update_version.sh b/tools/update_version.sh new file mode 100755 index 00000000..229b3ddd --- /dev/null +++ b/tools/update_version.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +new_version=$1 + +current_version=`grep -oP "(?<=VERSION \= \")([0-9.]+)(?=\")" cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java` +current_version_re=${current_version//./\\.} +echo "Current version is $current_version" +if [ -n "$new_version" ]; then + echo "New version will be $new_version" + echo "Pattern used: $current_version_re" + sed -e "s/${current_version_re}/${new_version}/g" -i "" cloudinary-core/src/main/java/com/cloudinary/Cloudinary.java + sed -e "s/${current_version_re}/${new_version}/g" -i "" README.md + sed -e "s/${current_version_re}/${new_version}/g" -i "" gradle.properties + git changelog -t $new_version +else + echo "Usage: $0 " + echo "For example: $0 1.9.2" +fi