├── .circleci ├── codecov.yml ├── config.yml └── settings.xml ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.md │ └── FEATURE_REQUEST.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── eclipse-formatter.xml ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── adobe │ │ └── cq │ │ └── commerce │ │ └── graphql │ │ └── client │ │ ├── CachingStrategy.java │ │ ├── GraphqlClient.java │ │ ├── GraphqlClientConfiguration.java │ │ ├── GraphqlRequest.java │ │ ├── GraphqlResponse.java │ │ ├── HttpMethod.java │ │ ├── RequestOptions.java │ │ ├── impl │ │ ├── CacheInvalidator.java │ │ ├── CacheKey.java │ │ ├── GraphqlClientAdapterFactory.java │ │ ├── GraphqlClientConfigurationImpl.java │ │ ├── GraphqlClientImpl.java │ │ ├── GraphqlClientMetrics.java │ │ └── GraphqlClientMetricsImpl.java │ │ └── package-info.java └── resources │ └── com │ └── adobe │ └── cq │ └── commerce │ └── graphql │ └── client │ └── version.properties └── test ├── java └── com │ └── adobe │ └── cq │ └── commerce │ └── graphql │ └── client │ ├── GraphqlClientTest.java │ ├── GraphqlRequestTest.java │ ├── RequestOptionsTest.java │ └── impl │ ├── CacheInvalidatorTest.java │ ├── CacheKeyTest.java │ ├── ConcurrencyTest.java │ ├── GraphqlAemContext.java │ ├── GraphqlClientAdapterFactoryTest.java │ ├── GraphqlClientConfigurationImplTest.java │ ├── GraphqlClientImplCachingTest.java │ ├── GraphqlClientImplMetricsTest.java │ ├── GraphqlClientImplTest.java │ ├── MockGraphqlClientConfiguration.java │ ├── MockServerHelper.java │ ├── ProtocolTest.java │ └── TestUtils.java └── resources ├── com └── adobe │ └── cq │ └── commerce │ └── graphql │ └── client │ └── version.properties ├── context └── graphql-client-adapter-factory-context.json ├── logback-test.xml ├── sample-graphql-request.json └── sample-graphql-response.json /.circleci/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | enabled: true 6 | target: 80% 7 | threshold: 10% 8 | base: auto 9 | patch: 10 | default: 11 | enabled: true 12 | target: 80% 13 | threshold: 10% 14 | base: auto -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: circleci/openjdk:8-jdk 6 | steps: 7 | - checkout 8 | - restore_cache: 9 | keys: 10 | - maven-repo-v1-{{ checksum "pom.xml" }} 11 | - maven-repo-v1- 12 | - run: 13 | name: Build 14 | command: | 15 | java -version 16 | mvn -v 17 | mvn -B clean install -s .circleci/settings.xml 18 | - save_cache: 19 | paths: 20 | - ~/.m2 21 | key: maven-repo-v1-{{ checksum "pom.xml" }} 22 | - store_test_results: 23 | path: target/surefire-reports 24 | - store_artifacts: 25 | path: target/surefire-reports 26 | - run: 27 | name: Upload Code Coverage 28 | command: bash <(curl -s https://codecov.io/bash) 29 | 30 | release: 31 | docker: 32 | - image: circleci/openjdk:8-jdk 33 | steps: 34 | - checkout 35 | - restore_cache: 36 | keys: 37 | - maven-repo-v1-{{ checksum "pom.xml" }} 38 | - maven-repo-v1- 39 | - run: 40 | name: Release 41 | # Only performs a 'mvn deploy' after the 'mvn release:prepare' because circleCI 42 | # already checks out the git tag like 'mvn release:perform' would do. 43 | command: | 44 | echo $GPG_PRIVATE_KEY | base64 --decode | gpg --batch --import 45 | mvn -B -s .circleci/settings.xml clean deploy -P release-sign-artifacts 46 | rm -rf /home/circleci/.gnupg 47 | 48 | # The 'release' workflow is trigged by the release tags committed by 'mvn release:prepare' 49 | workflows: 50 | version: 2 51 | build-and-release: 52 | jobs: 53 | - build: 54 | filters: 55 | tags: 56 | only: /.*/ 57 | - release: 58 | context: 59 | - CIF Maven Central 60 | requires: 61 | - build 62 | filters: 63 | branches: 64 | ignore: /.*/ 65 | tags: 66 | only: /^graphql-client-.*/ -------------------------------------------------------------------------------- /.circleci/settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | 18 | 19 | ossrh 20 | 21 | true 22 | 23 | 24 | gpg 25 | ${env.GPG_PASSPHRASE} 26 | 27 | 28 | 29 | 30 | 31 | 32 | ossrh 33 | ${env.SONATYPE_USER} 34 | ${env.SONATYPE_PASS} 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Adobe Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at Grp-opensourceoffice@adobe.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thanks for choosing to contribute! 4 | 5 | The following are a set of guidelines to follow when contributing to this project. 6 | 7 | ## Code Of Conduct 8 | 9 | This project adheres to the Adobe [code of conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. Please report unacceptable behavior to the team. 10 | 11 | ## Contributor License Agreement 12 | 13 | All third-party contributions to this project must be accompanied by a signed contributor license agreement. This gives Adobe permission to redistribute your contributions as part of the project. [Sign our CLA](http://opensource.adobe.com/cla.html). You only need to submit an Adobe CLA one time, so if you have submitted one previously, you are good to go! 14 | 15 | ## How to contribute 16 | 17 | New code contributions should be made primarily using GitHub pull requests. This involves creating a fork of the project in your personal space, adding your new code in a branch and triggering a pull request. 18 | 19 | See how to perform pull requests at https://help.github.com/articles/using-pull-requests. 20 | 21 | Please follow the [pull request template](PULL_REQUEST_TEMPLATE.md) when submitting a pull request! 22 | 23 | ## New Feature request 24 | Please follow the [feature template](ISSUE_TEMPLATE/FEATURE_REQUEST.md) to open new feature requests. 25 | 26 | 27 | ## Issues 28 | 29 | Please follow the [issue template](ISSUE_TEMPLATE/BUG_REPORT.md) to open new [issues](https://github.com/adobe/commerce-cif-graphql-client/issues) and join the conversations to provide feedback. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Issue report 3 | about: Having an issue? Please create a report to explain what it is about. 4 | 5 | --- 6 | 7 | ### Expected Behaviour 8 | 9 | ### Actual Behaviour 10 | 11 | ### Reproduce Scenario (including but not limited to) 12 | 13 | #### Steps to Reproduce 14 | 15 | #### Platform and Version 16 | 17 | #### Sample Code that illustrates the problem 18 | 19 | #### Logs taken while reproducing problem -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Do you have new ideas for this project? Please fill in a request. 4 | 5 | --- 6 | 7 | ### User Story 8 | 9 | 10 | 11 | ### Description & Motivation 12 | 13 | 14 | ### Deliverables 15 | 16 | 17 | 18 | 19 | 20 | ### Acceptance Criteria 21 | 22 | 23 | 24 | 25 | 26 | ### Verification Steps 27 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | 7 | ## Related Issue 8 | 9 | 10 | 11 | 12 | 13 | 14 | ## Motivation and Context 15 | 16 | 17 | 18 | ## How Has This Been Tested? 19 | 20 | 21 | 22 | 23 | 24 | ## Screenshots (if appropriate): 25 | 26 | ## Types of changes 27 | 28 | 29 | 30 | - [ ] Bug fix (non-breaking change which fixes an issue) 31 | - [ ] New feature (non-breaking change which adds functionality) 32 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 33 | 34 | ## Checklist: 35 | 36 | 37 | 38 | 39 | - [ ] I have signed the [Adobe Open Source CLA](http://opensource.adobe.com/cla.html). 40 | - [ ] My change requires a change to the documentation. 41 | - [ ] I have updated the documentation accordingly. 42 | - [ ] I have read the **CONTRIBUTING** document. 43 | - [ ] I have added tests to cover my changes and the overall coverage did not decrease. 44 | - [ ] All unit tests pass on CircleCi. 45 | - [ ] I ran all tests locally and they pass. 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | .classpath 4 | .metadata 5 | .project 6 | .settings 7 | /target/ 8 | pom.xml.releaseBackup 9 | release.properties 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/adobe/commerce-cif-graphql-client.svg?style=svg)](https://circleci.com/gh/adobe/commerce-cif-graphql-client) 2 | [![codecov](https://codecov.io/gh/adobe/commerce-cif-graphql-client/branch/master/graph/badge.svg)](https://codecov.io/gh/adobe/commerce-cif-graphql-client) 3 | [![Maven Central](https://img.shields.io/maven-central/v/com.adobe.commerce.cif/graphql-client.svg)](https://search.maven.org/search?q=g:com.adobe.commerce.cif%20AND%20a:graphql-client) 4 | 5 | # GraphQL client 6 | 7 | This project is a GraphQL client for AEM. It is an OSGi bundle that can be instantiated with an OSGi configuration in the AEM OSGi configuration console. It can also be instantiated directly with java code. 8 | 9 | ## Installation 10 | 11 | To build and install the latest version in a running AEM instance, simply do 12 | 13 | ``` 14 | mvn clean install sling:install 15 | ``` 16 | This installs everything by default to `localhost:4502` without any context path. You can also configure the install location with the following maven properties: 17 | * `aem.host`: the name of the AEM instance 18 | * `aem.port`: the port number of the AEM instance 19 | * `aem.contextPath`: the context path (if any) of your AEM instance, starting with `/` 20 | 21 | ## Using the GraphQL client 22 | 23 | To use this library in your project, just add the following maven dependency to your project and install the bundle in your AEM instance: 24 | 25 | ```xml 26 | 27 | com.adobe.commerce.cif 28 | graphql-client 29 | ... 30 | provided 31 | 32 | ``` 33 | 34 | You'll then have to setup and configure the client in your AEM instance. 35 | 36 | ## OSGi configuration 37 | 38 | To instantiate instances of this GraphQL client, simply go the AEM OSGi configuration console and look for "GraphQL Client Configuration Factory". Add a configuration and set the following mandatory parameters: 39 | * `identifier`: must be unique among all GraphQL clients. 40 | * `url`: the URL of the GraphQL server endpoint used by this client. 41 | * `httpMethod`: the default HTTP method used to send requests, can be either GET or POST. This can be overriden on a request basis. 42 | 43 | The `identifier` is used by the adapter factory to resolve clients via the `cq:graphqlClient` property set on any JCR node. When this is set on a resource or the resource ancestors, one can write `GraphqlClient client = resource.adaptTo(GraphqlClient.class);`. 44 | 45 | ### Caching 46 | 47 | Starting with version `1.4.0`, the client can cache GraphQL `query` requests based on the `cacheConfigurations` parameter. This supports the configuration of multiple caches, each being identified by a name and having its own set of caching parameters. This ensures that multiple components and services using cached data can have their own caching strategy and that those will not collide, for example when one component/service starves a common cache with a very large amount of requests. On the Java side, caching is controlled by the `CachingStrategy` class that can be set in the `RequestOptions` class. 48 | 49 | Because of the limitations of the AEM OSGi configuration console when defining multiple parameters with multiple values, each cache configuration entry must be specified with the following format: 50 | * `NAME:ENABLE:MAXSIZE:TIMEOUT` like for example `mycache:true:1000:60` where each attribute is defined as: 51 | * NAME (String) : the name of the cache 52 | * ENABLE (true|false) : enables or disables that cache entry (useful to temporarily disable a cache) 53 | * MAXSIZE (Integer) : the maximum size of the cache in number of entries 54 | * TIMEOUT (Integer) : the timeout for each cache entry, in seconds 55 | 56 | Note that even if it is enabled, a cache is only used if the `RequestOptions` set by the caller component/service defines the cache name and explicitly sets the `DataFetchingPolicy` to `CACHE_FIRST`. This ensures that the behavior of the client is backwards compatible (= caching is skipped by default). In addition, a caller can explicitly set the `DataFetchingPolicy` to `NETWORK_ONLY` to skip caching programmatically, even if caching is enabled in the OSGi configuration. 57 | 58 | ## Releases to Maven Central 59 | 60 | Releases are triggered by manually running `mvn release:prepare release:clean` on the `master` branch. This automatically pushes a commit with a release git tag like `graphql-client-x.y.z.` which triggers a dedicated `CircleCI` build that performs the deployment of the artifact to Maven Central. 61 | 62 | ## Code Formatting 63 | You can find the code formatting rules in the `eclipse-formatter.xml` file. The code formatting is automatically checked for each build. To automatically format your code, please run: 64 | ```bash 65 | mvn clean install -Pformat-code 66 | ``` 67 | 68 | ### Contributing 69 | 70 | Contributions are welcomed! Read the [Contributing Guide](.github/CONTRIBUTING.md) for more information. 71 | 72 | ### Licensing 73 | 74 | This project is licensed under the Apache V2 License. See [LICENSE](LICENSE) for more information. 75 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/CachingStrategy.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | public class CachingStrategy { 18 | 19 | private String cacheName; 20 | private DataFetchingPolicy dataFetchingPolicy; 21 | 22 | public String getCacheName() { 23 | return cacheName; 24 | } 25 | 26 | public CachingStrategy withCacheName(String cacheName) { 27 | this.cacheName = cacheName; 28 | return this; 29 | } 30 | 31 | public DataFetchingPolicy getDataFetchingPolicy() { 32 | return dataFetchingPolicy; 33 | } 34 | 35 | public CachingStrategy withDataFetchingPolicy(DataFetchingPolicy dataFetchingPolicy) { 36 | this.dataFetchingPolicy = dataFetchingPolicy; 37 | return this; 38 | } 39 | 40 | /** 41 | * The data fetching policy with respect to caching. 42 | */ 43 | public static enum DataFetchingPolicy { 44 | 45 | /** 46 | * Fetch data from the cache first. If the response doesn't exist or is expired, then fetch a response from the network. 47 | */ 48 | CACHE_FIRST, 49 | 50 | /** 51 | * Fetch data from the network only, ignoring any cached responses. 52 | */ 53 | NETWORK_ONLY 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/GraphqlClient.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.lang.reflect.Type; 18 | 19 | import org.osgi.annotation.versioning.ProviderType; 20 | 21 | @ProviderType 22 | public interface GraphqlClient { 23 | 24 | /** 25 | * Returns the identifier of this GraphQL client and backing OSGi service. 26 | * This can be set on JCR resources with the cq:graphqlClient property. 27 | * 28 | * @return The identifier value of this client. 29 | */ 30 | public String getIdentifier(); 31 | 32 | /** 33 | * Returns the URL of the used GraphQL server endpoint. 34 | * 35 | * @return The backend GraphQL server endpoint used by this client. 36 | */ 37 | public String getGraphQLEndpoint(); 38 | 39 | /** 40 | * Returns the complete configuration of the GraphQL client. 41 | * 42 | * @return GraphQL client configuration. 43 | */ 44 | public GraphqlClientConfiguration getConfiguration(); 45 | 46 | /** 47 | * Executes the given GraphQL request and deserializes the response data based on the types T and U. 48 | * The type T is used to deserialize the 'data' object of the GraphQL response, and the type U is used 49 | * to deserialize the 'errors' array of the GraphQL response. 50 | * Each generic type can be a simple class or a generic class. To specify a simple class, just do: 51 | * 52 | *
 53 |      * GraphqlResponse<MyData, MyError> response = graphqlClient.execute(request, MyData.class, MyError.class);
 54 |      * MyData data = response.getData();
 55 |      * List<MyError> errors = response.getErrors();
 56 |      * 
57 | * 58 | * To specify a generic type (usually for the type T), one can use 59 | * the {@link com.google.gson.reflect.TypeToken} class. For example: 60 | * 61 | *
 62 |      * Type typeOfT = new TypeToken<List<String>>() {}.getType();
 63 |      * GraphqlResponse<List<String>, MyError> response = graphqlClient.execute(request, typeOfT, MyError.class);
 64 |      * List<String> data = response.getData();
 65 |      * 
66 | * 67 | * @param request The GraphQL request. 68 | * @param typeOfT The type of the expected GraphQL response 'data' field. 69 | * @param typeOfU The type of the elements of the expected GraphQL response 'errors' field. 70 | * 71 | * @param The generic type of the 'data' object in the JSON GraphQL response. 72 | * @param The generic type of the elements of the 'errors' array in the JSON GraphQL response. 73 | * 74 | * @return A GraphQL response. 75 | * 76 | * @exception RuntimeException if the GraphQL HTTP request does not return 200 or if the JSON response cannot be parsed or deserialized. 77 | */ 78 | public GraphqlResponse execute(GraphqlRequest request, Type typeOfT, Type typeOfU); 79 | 80 | /** 81 | * Executes the given GraphQL request and deserializes the response data based on the types T and U. 82 | * The type T is used to deserialize the 'data' object of the GraphQL response, and the type U is used 83 | * to deserialize the 'errors' array of the GraphQL response. 84 | * Each generic type can be a simple class or a generic class. To specify a simple class, just do: 85 | * 86 | *
 87 |      * GraphqlResponse<MyData, MyError> response = graphqlClient.execute(request, MyData.class, MyError.class);
 88 |      * MyData data = response.getData();
 89 |      * List<MyError> errors = response.getErrors();
 90 |      * 
91 | * 92 | * To specify a generic type (usually for the type T), one can use 93 | * the {@link com.google.gson.reflect.TypeToken} class. For example: 94 | * 95 | *
 96 |      * Type typeOfT = new TypeToken<List<String>>() {}.getType();
 97 |      * GraphqlResponse<List<String>, MyError> response = graphqlClient.execute(request, typeOfT, MyError.class);
 98 |      * List<String> data = response.getData();
 99 |      * 
100 | * 101 | * @param request The GraphQL request. 102 | * @param typeOfT The type of the expected GraphQL response 'data' field. 103 | * @param typeOfU The type of the elements of the expected GraphQL response 'errors' field. 104 | * @param options An object holding options that can be set when executing the request. 105 | * 106 | * @param The generic type of the 'data' object in the JSON GraphQL response. 107 | * @param The generic type of the elements of the 'errors' array in the JSON GraphQL response. 108 | * 109 | * @return A GraphQL response. 110 | * 111 | * @exception RuntimeException if the GraphQL HTTP request does not return 200 or if the JSON response cannot be parsed or deserialized. 112 | */ 113 | public GraphqlResponse execute(GraphqlRequest request, Type typeOfT, Type typeOfU, RequestOptions options); 114 | 115 | /** 116 | * Delete cache entries from in-memory cache based on the params entries. 117 | * If all params are null or empty, all cache entries are deleted. 118 | * If some params are not null or empty, then based on its corresponding cache entries will be deleted. 119 | * 120 | * @param storeView The store view to invalidate cache for. 121 | * @param cacheNames The cache names to invalidate. 122 | * @param patterns The patterns to invalidate cache for. 123 | */ 124 | default void invalidateCache(String storeView, String[] cacheNames, String[] patterns) { 125 | // Default implementation does nothing 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/GraphqlClientConfiguration.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import org.osgi.service.metatype.annotations.AttributeDefinition; 18 | import org.osgi.service.metatype.annotations.AttributeType; 19 | import org.osgi.service.metatype.annotations.ObjectClassDefinition; 20 | 21 | @ObjectClassDefinition(name = "CIF GraphQL Client Configuration Factory") 22 | public @interface GraphqlClientConfiguration { 23 | 24 | String CQ_GRAPHQL_CLIENT = "cq:graphqlClient"; 25 | String DEFAULT_IDENTIFIER = "default"; 26 | 27 | int MAX_HTTP_CONNECTIONS_DEFAULT = 20; 28 | boolean ACCEPT_SELF_SIGNED_CERTIFICATES = false; 29 | boolean ALLOW_HTTP_PROTOCOL = false; 30 | 31 | int DEFAULT_CONNECTION_TIMEOUT = 5000; 32 | int DEFAULT_SOCKET_TIMEOUT = 5000; 33 | int DEFAULT_REQUESTPOOL_TIMEOUT = 2000; 34 | int DEFAULT_CONNECTION_KEEP_ALIVE = -1; 35 | int DEFAULT_CONNECTION_TTL = -1; 36 | 37 | @AttributeDefinition( 38 | name = "GraphQL Service Identifier", 39 | description = "A unique identifier for this GraphQL client, used by the JCR resource property " + CQ_GRAPHQL_CLIENT 40 | + " to identify the service.", 41 | type = AttributeType.STRING, 42 | required = true) 43 | String identifier() default DEFAULT_IDENTIFIER; 44 | 45 | @AttributeDefinition( 46 | name = "GraphQL Service URL", 47 | description = "The URL of the GraphQL server endpoint, this must be a secure host using HTTPS.", 48 | type = AttributeType.STRING, 49 | required = true) 50 | String url(); 51 | 52 | @AttributeDefinition( 53 | name = "Default HTTP method", 54 | description = "The default HTTP method used to send GraphQL requests.", 55 | required = true) 56 | HttpMethod httpMethod() default HttpMethod.POST; 57 | 58 | @AttributeDefinition( 59 | name = "Accept self-signed SSL certificates", 60 | description = "Enable insecure/developer mode to accept self-signed SSL certificates. Do NOT activate on production systems!", 61 | type = AttributeType.BOOLEAN) 62 | boolean acceptSelfSignedCertificates() default ACCEPT_SELF_SIGNED_CERTIFICATES; 63 | 64 | @AttributeDefinition( 65 | name = "Allow HTTP communication", 66 | description = "Enable insecure/developer mode to allow communication via HTTP. Do NOT activate on production systems!", 67 | type = AttributeType.BOOLEAN) 68 | boolean allowHttpProtocol() default ALLOW_HTTP_PROTOCOL; 69 | 70 | @AttributeDefinition( 71 | name = "Max HTTP connections", 72 | description = "The maximum number of concurrent HTTP connections the connector can make", 73 | type = AttributeType.INTEGER) 74 | int maxHttpConnections() default MAX_HTTP_CONNECTIONS_DEFAULT; 75 | 76 | @AttributeDefinition( 77 | name = "Http connection timeout", 78 | description = "The timeout in milliseconds until a connection is established. Is the timeout longer than the default timeout a " 79 | + "warning will be logged. Is it 0 or smaller it will fallback to the default timeout and a warning will be logged. Defaults " 80 | + "to " + DEFAULT_CONNECTION_TIMEOUT, 81 | type = AttributeType.INTEGER) 82 | int connectionTimeout() default DEFAULT_CONNECTION_TIMEOUT; 83 | 84 | @AttributeDefinition( 85 | name = "Http socket timeout", 86 | description = "The socket timeout in milliseconds, which is the timeout for waiting for data or, put differently, a maximum period " 87 | + "inactivity between two consecutive data packets. Is the timeout longer than the default timeout a warning will be logged. " 88 | + "Is it 0 or smaller it will fallback to the default timeout and a warning will be logged. Defaults to " 89 | + DEFAULT_SOCKET_TIMEOUT, 90 | type = AttributeType.INTEGER) 91 | int socketTimeout() default DEFAULT_SOCKET_TIMEOUT; 92 | 93 | @AttributeDefinition( 94 | name = "Request pool timeout", 95 | description = "The timeout in milliseconds used when requesting a connection from the connection manager. Is the timeout longer " 96 | + "than the default timeout a warning will be logged. Is it 0 or smaller it will fallback to the default timeout and a " 97 | + "warning will be logged. Defaults to " + DEFAULT_REQUESTPOOL_TIMEOUT, 98 | type = AttributeType.INTEGER) 99 | int requestPoolTimeout() default DEFAULT_REQUESTPOOL_TIMEOUT; 100 | 101 | @AttributeDefinition( 102 | name = "Connection keep alive timeout", 103 | description = "The maximum number of seconds an unused connections is kept alive in the connection pool after the last response. " 104 | + "If the value is < 0 then connections are kept alive indefinitely. If the value is 0 then connections will never be reused. " 105 | + "Defaults to " + DEFAULT_CONNECTION_KEEP_ALIVE, 106 | type = AttributeType.INTEGER) 107 | int connectionKeepAlive() default DEFAULT_CONNECTION_KEEP_ALIVE; 108 | 109 | @AttributeDefinition( 110 | name = "Connection time to live", 111 | description = "The maximum number of seconds a connections is reused for. If the value is < 0 then the connection may be reused " 112 | + "indefinitely, depending on the configured connection keep alive timeout. If the value is 0 then connections will never be " 113 | + "reused. Defaults to " + DEFAULT_CONNECTION_TTL, 114 | type = AttributeType.INTEGER) 115 | int connectionTtl() default DEFAULT_CONNECTION_TTL; 116 | 117 | @AttributeDefinition( 118 | name = "Default HTTP Headers", 119 | description = "HTTP Headers which shall be sent with each request. Might be used for authentication. The format of each header is " 120 | + "name:value", 121 | type = AttributeType.STRING) 122 | String[] httpHeaders(); 123 | 124 | @AttributeDefinition( 125 | name = "GraphQL cache configurations", 126 | description = "Each entry must follow the format NAME:ENABLE:MAXSIZE:TIMEOUT like for example product:true:1000:5 - " 127 | + "NAME (String) : the name of the cache - " 128 | + "ENABLE (true|false) : enables or disables the cache with that NAME - " 129 | + "MAXSIZE (Integer) : the maximum size of the cache in number of entries - " 130 | + "TIMEOUT (Integer) : the timeout for each cache entry, in seconds.", 131 | type = AttributeType.STRING) 132 | String[] cacheConfigurations(); 133 | 134 | @AttributeDefinition( 135 | name = "Ranking", 136 | description = "Integer value defining the ranking of this queue configuration. If more than one GraphQL Client use the same " 137 | + "identifier the one with the higher ranking will be used. Defaults to 0") 138 | int service_ranking() default 0; 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/GraphqlRequest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.util.Objects; 18 | 19 | import org.apache.commons.lang3.StringUtils; 20 | import org.apache.commons.lang3.builder.HashCodeBuilder; 21 | 22 | public class GraphqlRequest { 23 | 24 | protected String query; 25 | protected String operationName; 26 | protected Object variables; 27 | 28 | private Integer hash; 29 | 30 | public GraphqlRequest(String query) { 31 | this.query = query; 32 | } 33 | 34 | public String getQuery() { 35 | return query; 36 | } 37 | 38 | public void setQuery(String query) { 39 | this.query = query; 40 | } 41 | 42 | public String getOperationName() { 43 | return operationName; 44 | } 45 | 46 | public void setOperationName(String operationName) { 47 | this.operationName = operationName; 48 | } 49 | 50 | public Object getVariables() { 51 | return variables; 52 | } 53 | 54 | public void setVariables(Object variables) { 55 | this.variables = variables; 56 | } 57 | 58 | @Override 59 | public boolean equals(Object o) { 60 | if (this == o) { 61 | return true; 62 | } 63 | if (o == null || getClass() != o.getClass()) { 64 | return false; 65 | } 66 | GraphqlRequest that = (GraphqlRequest) o; 67 | if (!StringUtils.equals(query, that.query)) { 68 | return false; 69 | } 70 | if (!StringUtils.equals(operationName, that.operationName)) { 71 | return false; 72 | } 73 | return Objects.equals(variables, that.variables); 74 | } 75 | 76 | @Override 77 | public int hashCode() { 78 | if (hash != null) { 79 | return hash.intValue(); 80 | } 81 | hash = new HashCodeBuilder() 82 | .append(query) 83 | .append(operationName) 84 | .append(variables) 85 | .toHashCode(); 86 | return hash.intValue(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/GraphqlResponse.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.util.List; 18 | 19 | public class GraphqlResponse { 20 | 21 | protected T data; 22 | protected List errors; 23 | 24 | public T getData() { 25 | return data; 26 | } 27 | 28 | public void setData(T data) { 29 | this.data = data; 30 | } 31 | 32 | public List getErrors() { 33 | return errors; 34 | } 35 | 36 | public void setErrors(List errors) { 37 | this.errors = errors; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/HttpMethod.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | /** 18 | * Represents the allowed and supported HTTP methods to send GraphQL requests. 19 | */ 20 | public enum HttpMethod { 21 | POST, GET 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/RequestOptions.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.util.Comparator; 18 | import java.util.List; 19 | import java.util.Objects; 20 | import java.util.stream.Collectors; 21 | 22 | import org.apache.commons.collections.CollectionUtils; 23 | import org.apache.commons.lang3.StringUtils; 24 | import org.apache.commons.lang3.builder.HashCodeBuilder; 25 | import org.apache.http.Header; 26 | import org.apache.http.message.BasicHeader; 27 | 28 | import com.google.gson.Gson; 29 | 30 | /** 31 | * This class is used to set various options when executing a GraphQL request. 32 | */ 33 | public class RequestOptions { 34 | 35 | /** 36 | * To compare headers, we will sort them by name and value. 37 | */ 38 | private static final Comparator
HEADER_COMPARATOR = Comparator.comparing(Header::getName).thenComparing(Header::getValue); 39 | 40 | private Gson gson; 41 | private List
headers; 42 | private HttpMethod httpMethod; 43 | private CachingStrategy cachingStrategy; 44 | 45 | private Integer hash; 46 | 47 | /** 48 | * Sets the {@link Gson} instance that will be used to deserialise the JSON response. This should only be used when the JSON 49 | * response cannot be deserialised by a standard Gson instance, or when some custom deserialisation is needed. 50 | * 51 | * @param gson A custom {@link Gson} instance. 52 | * @return This RequestOptions object. 53 | */ 54 | public RequestOptions withGson(Gson gson) { 55 | this.gson = gson; 56 | return this; 57 | } 58 | 59 | /** 60 | * Permits to define HTTP headers that will be sent with the GraphQL request. 61 | * See {@link BasicHeader} for an implementation of the Header interface. 62 | * 63 | * @param headers The HTTP headers. 64 | * @return This RequestOptions object. 65 | */ 66 | public RequestOptions withHeaders(List
headers) { 67 | this.headers = headers; 68 | return this; 69 | } 70 | 71 | /** 72 | * Sets the HTTP method used to send the request, only GET or POST are supported. 73 | * By default, the client sends a POST request. If GET is used, the underlying HTTP client 74 | * will automatically URL-Encode the GraphQL query, operation name, and variables. 75 | * 76 | * @param httpMethod The HTTP method. 77 | * @return This RequestOptions object. 78 | */ 79 | public RequestOptions withHttpMethod(HttpMethod httpMethod) { 80 | this.httpMethod = httpMethod; 81 | return this; 82 | } 83 | 84 | public RequestOptions withCachingStrategy(CachingStrategy cachingStrategy) { 85 | this.cachingStrategy = cachingStrategy; 86 | return this; 87 | } 88 | 89 | public Gson getGson() { 90 | return gson; 91 | } 92 | 93 | public List
getHeaders() { 94 | return headers; 95 | } 96 | 97 | public HttpMethod getHttpMethod() { 98 | return httpMethod; 99 | } 100 | 101 | public CachingStrategy getCachingStrategy() { 102 | return cachingStrategy; 103 | } 104 | 105 | @Override 106 | public boolean equals(Object o) { 107 | if (this == o) { 108 | return true; 109 | } 110 | if (o == null || getClass() != o.getClass()) { 111 | return false; 112 | } 113 | RequestOptions that = (RequestOptions) o; 114 | if (!Objects.equals(httpMethod, that.httpMethod)) { 115 | return false; 116 | } 117 | 118 | if (CollectionUtils.isEmpty(headers) && CollectionUtils.isEmpty(that.headers)) { 119 | return true; 120 | } 121 | if ((headers == null) ^ (that.headers == null)) { // one is null but not the other 122 | return false; 123 | } 124 | if (headers.size() != that.headers.size()) { 125 | return false; 126 | } 127 | 128 | // We cannot use Objects.equals with lists because this checks object equality for all list elements 129 | // and elements must be in the same order. 130 | 131 | List
sortedHeaders = headers.stream() 132 | .sorted(HEADER_COMPARATOR) 133 | .collect(Collectors.toList()); 134 | 135 | List
thatSortedHeaders = that.headers.stream() 136 | .sorted(HEADER_COMPARATOR) 137 | .collect(Collectors.toList()); 138 | 139 | for (int i = 0, l = sortedHeaders.size(); i < l; i++) { 140 | Header header = sortedHeaders.get(i); 141 | Header thatHeader = thatSortedHeaders.get(i); 142 | if (!StringUtils.equals(header.getName(), thatHeader.getName())) { 143 | return false; 144 | } 145 | if (!StringUtils.equals(header.getValue(), thatHeader.getValue())) { 146 | return false; 147 | } 148 | } 149 | return true; 150 | } 151 | 152 | @Override 153 | public int hashCode() { 154 | if (hash != null) { 155 | return hash.intValue(); 156 | } 157 | HashCodeBuilder builder = new HashCodeBuilder(); 158 | builder.append(httpMethod); 159 | if (headers != null) { 160 | headers.stream() 161 | .sorted(HEADER_COMPARATOR) 162 | .forEach(h -> builder.append(h.getName()).append(h.getValue())); 163 | } else { 164 | builder.append(headers); 165 | } 166 | hash = builder.toHashCode(); 167 | return hash.intValue(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/impl/CacheInvalidator.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2024 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | package com.adobe.cq.commerce.graphql.client.impl; 15 | 16 | import java.util.Arrays; 17 | import java.util.List; 18 | import java.util.Map; 19 | import java.util.regex.Matcher; 20 | import java.util.regex.Pattern; 21 | 22 | import org.apache.commons.lang3.StringUtils; 23 | import org.apache.http.Header; 24 | import org.slf4j.Logger; 25 | import org.slf4j.LoggerFactory; 26 | 27 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 28 | import com.google.common.cache.Cache; 29 | import com.google.gson.Gson; 30 | import com.google.gson.GsonBuilder; 31 | 32 | class CacheInvalidator { 33 | private static final Logger LOGGER = LoggerFactory.getLogger(CacheInvalidator.class); 34 | private static final String STORE_HEADER_NAME = "Store"; 35 | private Map>> caches; 36 | private Gson gson; 37 | 38 | CacheInvalidator(Map>> caches) { 39 | this.caches = caches; 40 | this.gson = new GsonBuilder() 41 | .disableHtmlEscaping() 42 | .create(); 43 | } 44 | 45 | private boolean isAllParametersEmpty(String storeView, String[] cacheNames, String[] patterns) { 46 | return StringUtils.isBlank(storeView) && 47 | (cacheNames == null || cacheNames.length == 0) && 48 | (patterns == null || patterns.length == 0); 49 | } 50 | 51 | private boolean isOnlyStoreViewProvided(String storeView, String[] cacheNames, String[] patterns) { 52 | return !StringUtils.isBlank(storeView) && 53 | (cacheNames == null || cacheNames.length == 0) && 54 | (patterns == null || patterns.length == 0); 55 | } 56 | 57 | private boolean isCacheNamesProvided(String[] cacheNames, String[] patterns) { 58 | return (cacheNames != null && cacheNames.length > 0) && 59 | (patterns == null || patterns.length == 0); 60 | } 61 | 62 | /** 63 | * Invalidates cache entries based on the provided criteria. 64 | * 65 | * @param storeView The store view to match against cache entries 66 | * @param cacheNames Array of cache names to invalidate 67 | * @param patterns Array of regex patterns to match against cache entries 68 | */ 69 | void invalidateCache(String storeView, String[] cacheNames, String[] patterns) { 70 | if (isAllParametersEmpty(storeView, cacheNames, patterns)) { 71 | invalidateAll(); 72 | } else if (isOnlyStoreViewProvided(storeView, cacheNames, patterns)) { 73 | invalidateStoreView(storeView); 74 | } else if (isCacheNamesProvided(cacheNames, patterns)) { 75 | invalidateSpecificCaches(storeView, cacheNames); 76 | } else { 77 | for (String pattern : patterns) { 78 | if (pattern != null && !pattern.isEmpty()) { 79 | invalidateCacheBasedOnPattern(pattern, storeView, cacheNames); 80 | } else { 81 | LOGGER.debug("Skipping null pattern in patterns array"); 82 | } 83 | } 84 | } 85 | } 86 | 87 | private void invalidateStoreView(String storeView) { 88 | caches.forEach((cacheName, cache) -> { 89 | cache.asMap().entrySet().stream() 90 | .filter(entry -> checkIfStorePresent(storeView, entry.getKey())) 91 | .forEach(entry -> { 92 | LOGGER.debug("Invalidating key based StoreView: {} in cache: {}", entry.getKey(), cacheName); 93 | cache.invalidate(entry.getKey()); 94 | }); 95 | }); 96 | } 97 | 98 | private void invalidateAll() { 99 | LOGGER.debug("Invalidating all caches..."); 100 | caches.values().forEach(Cache::invalidateAll); 101 | } 102 | 103 | private void invalidateSpecificCaches(String storeView, String[] cacheNames) { 104 | for (String cacheName : cacheNames) { 105 | Cache> cache = caches.get(cacheName); 106 | if (cache != null) { 107 | if (storeView == null) { 108 | LOGGER.debug("Invalidating entire cache: {}", cacheName); 109 | cache.invalidateAll(); 110 | } else { 111 | cache.asMap().entrySet().stream() 112 | .filter(entry -> checkIfStorePresent(storeView, entry.getKey())) 113 | .forEach(entry -> { 114 | LOGGER.debug("Invalidating key based SpecificCaches & storeView: {} in cache: {}", entry.getKey(), cacheName); 115 | cache.invalidate(entry.getKey()); 116 | }); 117 | } 118 | } else { 119 | LOGGER.debug("Cache not found: {}", cacheName); 120 | } 121 | } 122 | } 123 | 124 | private void invalidateCacheBasedOnPattern(String pattern, String storeView, String[] listOfCacheToSearch) { 125 | Pattern regex = Pattern.compile(pattern); 126 | caches.forEach((cacheName, cache) -> { 127 | if (listOfCacheToSearch != null && listOfCacheToSearch.length > 0 128 | && !Arrays.asList(listOfCacheToSearch).contains(cacheName)) { 129 | return; 130 | } 131 | cache.asMap().entrySet().stream() 132 | .filter(entry -> { 133 | if (!checkIfStorePresent(storeView, entry.getKey())) { 134 | return false; 135 | } 136 | GraphqlResponse value = entry.getValue(); 137 | String jsonResponse = gson.toJson(value); 138 | Matcher matcher = regex.matcher(jsonResponse); 139 | return matcher.find(); 140 | }) 141 | .forEach(entry -> { 142 | LOGGER.debug("Invalidating key: {} in cache based on pattern: {}", entry.getKey(), cacheName); 143 | cache.invalidate(entry.getKey()); 144 | }); 145 | }); 146 | } 147 | 148 | private boolean checkIfStorePresent(String storeView, CacheKey cacheKey) { 149 | if (storeView == null || cacheKey == null || cacheKey.getRequestOptions() == null) { 150 | return false; 151 | } 152 | List
headers = cacheKey.getRequestOptions().getHeaders(); 153 | if (headers != null && !headers.isEmpty()) { 154 | return headers.stream() 155 | .anyMatch( 156 | header -> STORE_HEADER_NAME.equalsIgnoreCase(header.getName()) 157 | && storeView.equalsIgnoreCase(header.getValue())); 158 | } 159 | return false; 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/impl/CacheKey.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.util.Objects; 18 | 19 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 20 | import com.adobe.cq.commerce.graphql.client.RequestOptions; 21 | 22 | public class CacheKey { 23 | 24 | private GraphqlRequest request; 25 | private RequestOptions options; 26 | private Integer hash; 27 | 28 | CacheKey(GraphqlRequest request, RequestOptions options) { 29 | this.request = request; 30 | this.options = options; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | if (hash == null) { 36 | hash = Objects.hash(request, options); 37 | } 38 | return hash.intValue(); 39 | } 40 | 41 | @Override 42 | public boolean equals(Object o) { 43 | if (this == o) { 44 | return true; 45 | } 46 | if (o == null || getClass() != o.getClass()) { 47 | return false; 48 | } 49 | CacheKey that = (CacheKey) o; 50 | return Objects.equals(request, that.request) && Objects.equals(options, that.options); 51 | } 52 | 53 | public GraphqlRequest getGraphqlRequest() { 54 | return request; 55 | } 56 | 57 | public RequestOptions getRequestOptions() { 58 | return options; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientAdapterFactory.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.util.Collections; 18 | import java.util.HashMap; 19 | import java.util.Map; 20 | import java.util.NavigableSet; 21 | import java.util.concurrent.ConcurrentSkipListSet; 22 | 23 | import org.apache.commons.lang3.StringUtils; 24 | import org.apache.sling.api.adapter.AdapterFactory; 25 | import org.apache.sling.api.resource.Resource; 26 | import org.apache.sling.api.resource.ValueMap; 27 | import org.apache.sling.commons.osgi.Order; 28 | import org.apache.sling.commons.osgi.ServiceUtil; 29 | import org.osgi.service.component.annotations.Component; 30 | import org.osgi.service.component.annotations.Reference; 31 | import org.osgi.service.component.annotations.ReferenceCardinality; 32 | import org.osgi.service.component.annotations.ReferencePolicy; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | import com.adobe.cq.commerce.graphql.client.GraphqlClient; 37 | import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration; 38 | import com.day.cq.commons.inherit.ComponentInheritanceValueMap; 39 | import com.day.cq.commons.inherit.HierarchyNodeInheritanceValueMap; 40 | import com.day.cq.commons.inherit.InheritanceValueMap; 41 | import com.day.cq.wcm.api.Page; 42 | import com.day.cq.wcm.api.PageManager; 43 | 44 | @Component( 45 | service = { AdapterFactory.class }, 46 | property = { 47 | AdapterFactory.ADAPTABLE_CLASSES + "=" + GraphqlClientAdapterFactory.RESOURCE_CLASS_NAME, 48 | AdapterFactory.ADAPTER_CLASSES + "=" + GraphqlClientAdapterFactory.GRAPHQLCLIENT_CLASS_NAME }) 49 | public class GraphqlClientAdapterFactory implements AdapterFactory { 50 | 51 | protected static final String RESOURCE_CLASS_NAME = "org.apache.sling.api.resource.Resource"; 52 | protected static final String GRAPHQLCLIENT_CLASS_NAME = "com.adobe.cq.commerce.graphql.client.GraphqlClient"; 53 | protected static final String CONFIG_NAME = "cloudconfigs/commerce"; 54 | 55 | private static final Logger LOGGER = LoggerFactory.getLogger(GraphqlClientAdapterFactory.class); 56 | 57 | protected NavigableSet clients = new ConcurrentSkipListSet<>(); 58 | private transient Map clientsByIdentifier = Collections.emptyMap(); 59 | 60 | @Reference( 61 | service = GraphqlClient.class, 62 | bind = "bindGraphqlClient", 63 | unbind = "unbindGraphqlClient", 64 | cardinality = ReferenceCardinality.AT_LEAST_ONE, 65 | policy = ReferencePolicy.DYNAMIC) 66 | protected void bindGraphqlClient(GraphqlClient graphqlClient, Map properties) { 67 | String identifier = graphqlClient.getIdentifier(); 68 | LOGGER.info("Registering GraphqlClient '{}'", identifier); 69 | clients.add(new Holder(graphqlClient, properties)); 70 | rebuildClientsByIdentifier(); 71 | } 72 | 73 | protected void unbindGraphqlClient(GraphqlClient graphqlClient) { 74 | String identifier = graphqlClient.getIdentifier(); 75 | LOGGER.info("De-registering GraphqlClient '{}'", identifier); 76 | clients.removeIf(holder -> holder.graphqlClient.equals(graphqlClient)); 77 | rebuildClientsByIdentifier(); 78 | } 79 | 80 | private void rebuildClientsByIdentifier() { 81 | Map newClientsByIdentifier = new HashMap<>(clients.size()); 82 | 83 | for (Holder holder : clients) { 84 | newClientsByIdentifier.putIfAbsent(holder.graphqlClient.getIdentifier(), holder.graphqlClient); 85 | } 86 | 87 | clientsByIdentifier = Collections.unmodifiableMap(newClientsByIdentifier); 88 | } 89 | 90 | @SuppressWarnings("unchecked") 91 | @Override 92 | public AdapterType getAdapter(Object adaptable, Class type) { 93 | if (!(adaptable instanceof Resource) || type != GraphqlClient.class) { 94 | return null; 95 | } 96 | 97 | Resource res = (Resource) adaptable; 98 | ValueMap properties = res.getValueMap(); 99 | 100 | String identifier = properties.get(GraphqlClientConfiguration.CQ_GRAPHQL_CLIENT, String.class); 101 | if (identifier == null) { 102 | identifier = readFallbackConfiguration(res); 103 | } 104 | 105 | if (StringUtils.isEmpty(identifier)) { 106 | LOGGER.error("Could not find {} property for given resource at {}", GraphqlClientConfiguration.CQ_GRAPHQL_CLIENT, res 107 | .getPath()); 108 | return null; 109 | } 110 | 111 | GraphqlClient client = clientsByIdentifier.get(identifier); 112 | 113 | if (client == null) { 114 | LOGGER.error("No GraphqlClient instance available for catalog identifier '{}'", identifier); 115 | return null; 116 | } 117 | 118 | return (AdapterType) client; 119 | } 120 | 121 | private String readFallbackConfiguration(Resource resource) { 122 | InheritanceValueMap properties; 123 | Page page = resource.getResourceResolver() 124 | .adaptTo(PageManager.class) 125 | .getContainingPage(resource); 126 | if (page != null) { 127 | properties = new HierarchyNodeInheritanceValueMap(page.getContentResource()); 128 | } else { 129 | properties = new ComponentInheritanceValueMap(resource); 130 | } 131 | return properties.getInherited(GraphqlClientConfiguration.CQ_GRAPHQL_CLIENT, String.class); 132 | } 133 | 134 | private static class Holder implements Comparable { 135 | 136 | private final GraphqlClient graphqlClient; 137 | private final Comparable comparable; 138 | 139 | public Holder(GraphqlClient graphqlClient, Map properties) { 140 | this.graphqlClient = graphqlClient; 141 | this.comparable = ServiceUtil.getComparableForServiceRanking(properties, Order.DESCENDING); 142 | } 143 | 144 | @Override 145 | public int compareTo(Holder o) { 146 | return comparable.compareTo(o.comparable); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientConfigurationImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2021 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | package com.adobe.cq.commerce.graphql.client.impl; 15 | 16 | import java.lang.annotation.Annotation; 17 | 18 | import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration; 19 | import com.adobe.cq.commerce.graphql.client.HttpMethod; 20 | 21 | /** 22 | * A mutable implementation of {@link GraphqlClientConfiguration}. 23 | */ 24 | class GraphqlClientConfigurationImpl implements Annotation, GraphqlClientConfiguration { 25 | 26 | private String url; 27 | private String identifier = GraphqlClientConfiguration.DEFAULT_IDENTIFIER; 28 | private HttpMethod httpMethod = HttpMethod.POST; 29 | private boolean acceptSelfSignedCertificates = GraphqlClientConfiguration.ACCEPT_SELF_SIGNED_CERTIFICATES; 30 | private boolean allowHttpProtocol = GraphqlClientConfiguration.ALLOW_HTTP_PROTOCOL; 31 | private int maxHttpConnections = GraphqlClientConfiguration.MAX_HTTP_CONNECTIONS_DEFAULT; 32 | private int connectionTimeout = GraphqlClientConfiguration.DEFAULT_CONNECTION_TIMEOUT; 33 | private int socketTimeout = GraphqlClientConfiguration.DEFAULT_SOCKET_TIMEOUT; 34 | private int requestPoolTimeout = GraphqlClientConfiguration.DEFAULT_REQUESTPOOL_TIMEOUT; 35 | private String[] httpHeaders = new String[0]; 36 | private String[] cacheConfigurations = new String[0]; 37 | private int connectionKeepAlive = GraphqlClientConfiguration.DEFAULT_CONNECTION_KEEP_ALIVE; 38 | private int connectionTtl = GraphqlClientConfiguration.DEFAULT_CONNECTION_TTL; 39 | private int serviceRanking = 0; 40 | 41 | GraphqlClientConfigurationImpl(String url) { 42 | this.url = url; 43 | } 44 | 45 | GraphqlClientConfigurationImpl(GraphqlClientConfiguration configuration) { 46 | identifier = configuration.identifier(); 47 | url = configuration.url(); 48 | httpMethod = configuration.httpMethod(); 49 | acceptSelfSignedCertificates = configuration.acceptSelfSignedCertificates(); 50 | allowHttpProtocol = configuration.allowHttpProtocol(); 51 | maxHttpConnections = configuration.maxHttpConnections(); 52 | connectionTimeout = configuration.connectionTimeout(); 53 | socketTimeout = configuration.socketTimeout(); 54 | requestPoolTimeout = configuration.requestPoolTimeout(); 55 | httpHeaders = configuration.httpHeaders() != null ? configuration.httpHeaders() : this.httpHeaders; 56 | cacheConfigurations = configuration.cacheConfigurations() != null ? configuration.cacheConfigurations() : this.cacheConfigurations; 57 | connectionKeepAlive = configuration.connectionKeepAlive(); 58 | connectionTtl = configuration.connectionTtl(); 59 | serviceRanking = configuration.service_ranking(); 60 | } 61 | 62 | @Override 63 | public Class annotationType() { 64 | return GraphqlClientConfiguration.class; 65 | } 66 | 67 | @Override 68 | public String identifier() { 69 | return identifier; 70 | } 71 | 72 | public void setIdentifier(String identifier) { 73 | this.identifier = identifier; 74 | } 75 | 76 | @Override 77 | public String url() { 78 | return url; 79 | } 80 | 81 | public void setUrl(String url) { 82 | this.url = url; 83 | } 84 | 85 | @Override 86 | public HttpMethod httpMethod() { 87 | return httpMethod; 88 | } 89 | 90 | public void setHttpMethod(HttpMethod httpMethod) { 91 | this.httpMethod = httpMethod; 92 | } 93 | 94 | @Override 95 | public boolean acceptSelfSignedCertificates() { 96 | return acceptSelfSignedCertificates; 97 | } 98 | 99 | public void setAcceptSelfSignedCertificates(boolean acceptSelfSignedCertificates) { 100 | this.acceptSelfSignedCertificates = acceptSelfSignedCertificates; 101 | } 102 | 103 | @Override 104 | public boolean allowHttpProtocol() { 105 | return allowHttpProtocol; 106 | } 107 | 108 | public void setAllowHttpProtocol(boolean allowHttpProtocol) { 109 | this.allowHttpProtocol = allowHttpProtocol; 110 | } 111 | 112 | @Override 113 | public int maxHttpConnections() { 114 | return maxHttpConnections; 115 | } 116 | 117 | public void setMaxHttpConnections(int maxHttpConnections) { 118 | this.maxHttpConnections = maxHttpConnections; 119 | } 120 | 121 | @Override 122 | public int connectionTimeout() { 123 | return connectionTimeout; 124 | } 125 | 126 | public void setConnectionTimeout(int connectionTimeout) { 127 | this.connectionTimeout = connectionTimeout; 128 | } 129 | 130 | @Override 131 | public int socketTimeout() { 132 | return socketTimeout; 133 | } 134 | 135 | public void setSocketTimeout(int socketTimeout) { 136 | this.socketTimeout = socketTimeout; 137 | } 138 | 139 | @Override 140 | public int requestPoolTimeout() { 141 | return requestPoolTimeout; 142 | } 143 | 144 | public void setRequestPoolTimeout(int requestPoolTimeout) { 145 | this.requestPoolTimeout = requestPoolTimeout; 146 | } 147 | 148 | @Override 149 | public String[] httpHeaders() { 150 | return httpHeaders; 151 | } 152 | 153 | public void setHttpHeaders(String... httpHeaders) { 154 | this.httpHeaders = httpHeaders; 155 | } 156 | 157 | @Override 158 | public String[] cacheConfigurations() { 159 | return cacheConfigurations; 160 | } 161 | 162 | public void setCacheConfigurations(String... cacheConfigurations) { 163 | this.cacheConfigurations = cacheConfigurations; 164 | } 165 | 166 | @Override 167 | public int connectionKeepAlive() { 168 | return connectionKeepAlive; 169 | } 170 | 171 | public void setConnectionKeepAlive(int connectionKeepAlive) { 172 | this.connectionKeepAlive = connectionKeepAlive; 173 | } 174 | 175 | @Override 176 | public int connectionTtl() { 177 | return connectionTtl; 178 | } 179 | 180 | public void setConnectionTtl(int connectionTtl) { 181 | this.connectionTtl = connectionTtl; 182 | } 183 | 184 | @Override 185 | public int service_ranking() { 186 | return serviceRanking; 187 | } 188 | 189 | public void setServiceRanking(int serviceRanking) { 190 | this.serviceRanking = serviceRanking; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientMetrics.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2021 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | package com.adobe.cq.commerce.graphql.client.impl; 15 | 16 | import java.util.function.Supplier; 17 | 18 | import com.codahale.metrics.Timer; 19 | 20 | /** 21 | * This interface provides a facade of the metrics tracked by the {@link GraphqlClientImpl}. With {@link GraphqlClientMetrics#NOOP} it 22 | * provides a no-operation implementation for environments that don't have cif metrics enabled. 23 | */ 24 | interface GraphqlClientMetrics { 25 | 26 | String REQUEST_DURATION_METRIC = "graphql-client.request.duration"; 27 | String REQUEST_ERROR_COUNT_METRIC = "graphql-client.request.errors"; 28 | String CACHE_HIT_METRIC = "graphql-client.cache.hits"; 29 | String CACHE_MISS_METRIC = "graphql-client.cache.misses"; 30 | String CACHE_EVICTION_METRIC = "graphql-client.cache.evictions"; 31 | String CACHE_USAGE_METRIC = "graphql-client.cache.usage"; 32 | String CONNECTION_POOL_AVAILABLE_METRIC = "graphql-client.connection-pool.available-connections"; 33 | String CONNECTION_POOL_PENDING_METRIC = "graphql-client.connection-pool.pending-requests"; 34 | String CONNECTION_POOL_USAGE_METRIC = "graphql-client.connection-pool.usage"; 35 | 36 | GraphqlClientMetrics NOOP = new GraphqlClientMetrics() { 37 | 38 | @Override 39 | public void addConnectionPoolMetric(String metricName, Supplier valueSupplier) { 40 | // do nothing 41 | } 42 | 43 | @Override 44 | public void addCacheMetric(String metricName, String cacheName, Supplier valueSupplier) { 45 | // do nothing 46 | } 47 | 48 | @Override 49 | public Supplier startRequestDurationTimer() { 50 | return () -> null; 51 | } 52 | 53 | @Override 54 | public void incrementRequestErrors() { 55 | // do nothing 56 | } 57 | 58 | @Override 59 | public void incrementRequestErrors(int status) { 60 | // do nothing 61 | } 62 | }; 63 | 64 | /** 65 | * Adds a connection pool metric. 66 | */ 67 | void addConnectionPoolMetric(String metricName, Supplier valueSupplier); 68 | 69 | /** 70 | * Adds a cache metric. 71 | */ 72 | void addCacheMetric(String metricName, String cacheName, Supplier valueSupplier); 73 | 74 | /** 75 | * Starts a request duration timer. The returned {@link Runnable} wraps the {@link Timer.Context#close()} and must be called in 76 | * order to add the measurement to the metric. 77 | * 78 | * @return 79 | */ 80 | Supplier startRequestDurationTimer(); 81 | 82 | /** 83 | * Increments the generic request error count. 84 | */ 85 | void incrementRequestErrors(); 86 | 87 | /** 88 | * Increments the specific request error count for the given status. 89 | * 90 | * @param 91 | */ 92 | void incrementRequestErrors(int status); 93 | 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientMetricsImpl.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2021 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | package com.adobe.cq.commerce.graphql.client.impl; 15 | 16 | import java.io.Closeable; 17 | import java.util.LinkedList; 18 | import java.util.List; 19 | import java.util.concurrent.ConcurrentHashMap; 20 | import java.util.concurrent.ConcurrentMap; 21 | import java.util.function.Supplier; 22 | 23 | import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration; 24 | import com.codahale.metrics.Counter; 25 | import com.codahale.metrics.MetricRegistry; 26 | import com.codahale.metrics.Timer; 27 | 28 | class GraphqlClientMetricsImpl implements GraphqlClientMetrics, Closeable { 29 | 30 | private static final String METRIC_LABEL_IDENTIFIER = "gql_client_identifier"; 31 | private static final String METRIC_LABEL_ENDPOINT = "gql_client_endpoint"; 32 | private static final String METRIC_LABEL_STATUS_CODE = "gql_response_status"; 33 | private static final String METRIC_LABEL_CACHE_NAME = "gql_cache_name"; 34 | 35 | private final MetricRegistry metrics; 36 | private final GraphqlClientConfiguration configuration; 37 | private final Timer requestDurationTimer; 38 | private final ConcurrentMap requestErrorCounters; 39 | private final List disposables = new LinkedList<>(); 40 | 41 | GraphqlClientMetricsImpl(MetricRegistry metrics, GraphqlClientConfiguration configuration) { 42 | this.metrics = metrics; 43 | this.configuration = configuration; 44 | this.requestDurationTimer = metrics.timer(REQUEST_DURATION_METRIC 45 | + ';' + METRIC_LABEL_ENDPOINT + '=' + configuration.url()); 46 | this.requestErrorCounters = new ConcurrentHashMap<>(); 47 | } 48 | 49 | @Override 50 | public void close() { 51 | disposables.forEach(Runnable::run); 52 | disposables.clear(); 53 | } 54 | 55 | @Override 56 | public void addConnectionPoolMetric(String metricName, Supplier valueSupplier) { 57 | String metricNameAndLabels = metricName 58 | + ';' + METRIC_LABEL_IDENTIFIER + '=' + configuration.identifier(); 59 | metrics.gauge(metricNameAndLabels, () -> valueSupplier::get); 60 | disposables.add(() -> metrics.remove(metricNameAndLabels)); 61 | } 62 | 63 | @Override 64 | public void addCacheMetric(String metricName, String cacheName, Supplier valueSupplier) { 65 | String metricNameAndLabels = metricName 66 | + ';' + METRIC_LABEL_IDENTIFIER + '=' + configuration.identifier() 67 | + ';' + METRIC_LABEL_CACHE_NAME + '=' + cacheName; 68 | metrics.gauge(metricNameAndLabels, () -> valueSupplier::get); 69 | disposables.add(() -> metrics.remove(metricNameAndLabels)); 70 | } 71 | 72 | @Override 73 | public Supplier startRequestDurationTimer() { 74 | return requestDurationTimer.time()::stop; 75 | } 76 | 77 | @Override 78 | public void incrementRequestErrors() { 79 | incrementRequestErrors(0); 80 | } 81 | 82 | @Override 83 | public void incrementRequestErrors(int status) { 84 | requestErrorCounters.computeIfAbsent(status, k -> { 85 | StringBuilder name = new StringBuilder(); 86 | name.append(REQUEST_ERROR_COUNT_METRIC); 87 | name.append(';').append(METRIC_LABEL_ENDPOINT).append('=').append(configuration.url()); 88 | if (status > 0) { 89 | name.append(';').append(METRIC_LABEL_STATUS_CODE).append('=').append(status); 90 | } 91 | return metrics.counter(name.toString()); 92 | }).inc(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/com/adobe/cq/commerce/graphql/client/package-info.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | @org.osgi.annotation.versioning.Version("1.11.0") 16 | package com.adobe.cq.commerce.graphql.client; 17 | -------------------------------------------------------------------------------- /src/main/resources/com/adobe/cq/commerce/graphql/client/version.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Adobe. All rights reserved. 3 | # This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. You may obtain a copy 5 | # of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software distributed under 8 | # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | # OF ANY KIND, either express or implied. See the License for the specific language 10 | # governing permissions and limitations under the License. 11 | # 12 | info.module = CifGraphqlClient 13 | info.release = ${pom.version} -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/GraphqlClientTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.lang.reflect.Type; 18 | 19 | import org.junit.Test; 20 | 21 | import static org.junit.Assert.fail; 22 | 23 | public class GraphqlClientTest { 24 | 25 | @Test 26 | public void testInvalidateCacheDefaultImplementation() { 27 | GraphqlClient graphqlClient = new GraphqlClient() { 28 | // Provide empty implementations for the other methods 29 | @Override 30 | public String getIdentifier() { 31 | return null; 32 | } 33 | 34 | @Override 35 | public String getGraphQLEndpoint() { 36 | return null; 37 | } 38 | 39 | @Override 40 | public GraphqlClientConfiguration getConfiguration() { 41 | return null; 42 | } 43 | 44 | @Override 45 | public GraphqlResponse execute(GraphqlRequest request, Type typeOfT, Type typeOfU, RequestOptions options) { 46 | return null; 47 | } 48 | 49 | @Override 50 | public GraphqlResponse execute(GraphqlRequest request, Type typeOfT, Type typeOfU) { 51 | return null; 52 | } 53 | 54 | }; 55 | 56 | // Call the default invalidateCache method and assert no exceptions are thrown 57 | try { 58 | graphqlClient.invalidateCache("default", new String[] { "cache1" }, new String[] { "pattern1" }); 59 | } catch (Exception e) { 60 | fail("invalidateCache method should not throw any exception"); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/GraphqlRequestTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | import java.util.TreeMap; 20 | 21 | import org.junit.Assert; 22 | import org.junit.Test; 23 | 24 | public class GraphqlRequestTest { 25 | 26 | @Test 27 | public void testEquals() { 28 | GraphqlRequest r1 = new GraphqlRequest("{something}"); 29 | r1.setOperationName("operation"); 30 | Map var1 = new HashMap<>(); 31 | var1.put("key1", "value1"); 32 | var1.put("key2", "value2"); 33 | r1.setVariables(var1); 34 | 35 | GraphqlRequest r2 = new GraphqlRequest("{something}"); 36 | r2.setOperationName("operation"); 37 | Map var2 = new TreeMap<>(); 38 | var2.put("key2", "value2"); 39 | var2.put("key1", "value1"); 40 | r2.setVariables(var2); 41 | 42 | Assert.assertEquals(r1.hashCode(), r2.hashCode()); 43 | Assert.assertTrue(r1.equals(r2)); 44 | 45 | Assert.assertEquals(r1.hashCode(), r1.hashCode()); 46 | Assert.assertTrue(r1.equals(r1)); 47 | Assert.assertFalse(r1.equals("wrongclass")); 48 | Assert.assertFalse(r1.equals(null)); 49 | } 50 | 51 | @Test 52 | public void testNotEqualsDifferentQueries() { 53 | GraphqlRequest r1 = new GraphqlRequest("{something}"); 54 | GraphqlRequest r2 = new GraphqlRequest("{else}"); 55 | 56 | Assert.assertNotEquals(r1.hashCode(), r2.hashCode()); 57 | Assert.assertFalse(r1.equals(r2)); 58 | } 59 | 60 | @Test 61 | public void testNotEqualsDifferentOperationNames() { 62 | GraphqlRequest r1 = new GraphqlRequest("{something}"); 63 | r1.setOperationName("operation1"); 64 | 65 | GraphqlRequest r2 = new GraphqlRequest("{something}"); 66 | r2.setOperationName("operation2"); 67 | 68 | Assert.assertNotEquals(r1.hashCode(), r2.hashCode()); 69 | Assert.assertFalse(r1.equals(r2)); 70 | } 71 | 72 | @Test 73 | public void testNotEqualsDifferentVariables() { 74 | GraphqlRequest r1 = new GraphqlRequest("{something}"); 75 | r1.setOperationName("operation"); 76 | Map var1 = new HashMap<>(); 77 | var1.put("key1", "value1"); 78 | var1.put("key2", "value2"); 79 | r1.setVariables(var1); 80 | 81 | GraphqlRequest r2 = new GraphqlRequest("{something}"); 82 | r2.setOperationName("operation"); 83 | Map var2 = new TreeMap<>(); 84 | var2.put("key2", "value2"); 85 | var2.put("key3", "value3"); 86 | r2.setVariables(var2); 87 | 88 | Assert.assertNotEquals(r1.hashCode(), r2.hashCode()); 89 | Assert.assertFalse(r1.equals(r2)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/RequestOptionsTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client; 16 | 17 | import java.util.ArrayList; 18 | import java.util.Collections; 19 | import java.util.LinkedList; 20 | import java.util.List; 21 | 22 | import org.apache.http.Header; 23 | import org.apache.http.message.BasicHeader; 24 | import org.junit.Assert; 25 | import org.junit.Test; 26 | 27 | public class RequestOptionsTest { 28 | 29 | @Test 30 | public void testEqualsEmptyRequestOptions() { 31 | RequestOptions opt1 = new RequestOptions(); 32 | RequestOptions opt2 = new RequestOptions(); 33 | 34 | Assert.assertEquals(opt1.hashCode(), opt2.hashCode()); 35 | Assert.assertTrue(opt1.equals(opt2)); 36 | 37 | Assert.assertEquals(opt1.hashCode(), opt1.hashCode()); 38 | Assert.assertTrue(opt1.equals(opt1)); 39 | Assert.assertFalse(opt1.equals("wrongclass")); 40 | Assert.assertFalse(opt1.equals(null)); 41 | } 42 | 43 | @Test 44 | public void testEqualsEmptyHeadersList() { 45 | RequestOptions opt1 = new RequestOptions(); 46 | opt1.withHeaders(new ArrayList<>()); 47 | RequestOptions opt2 = new RequestOptions(); 48 | opt2.withHeaders(new LinkedList<>()); 49 | 50 | Assert.assertEquals(opt1.hashCode(), opt2.hashCode()); 51 | Assert.assertTrue(opt1.equals(opt2)); 52 | } 53 | 54 | @Test 55 | public void testEquals() { 56 | RequestOptions opt1 = new RequestOptions(); 57 | opt1.withHttpMethod(HttpMethod.GET); 58 | List
headers1 = new ArrayList<>(); 59 | headers1.add(new BasicHeader("Store", "default")); 60 | headers1.add(new BasicHeader("Authentication", "Bearer 1234")); 61 | headers1.add(new BasicHeader("Authentication", "Basic abcd")); 62 | opt1.withHeaders(headers1); 63 | 64 | RequestOptions opt2 = new RequestOptions(); 65 | opt2.withHttpMethod(HttpMethod.GET); 66 | List
headers2 = new LinkedList<>(); 67 | headers2.add(new BasicHeader("Authentication", "Basic abcd")); // Same headers but different order 68 | headers2.add(new BasicHeader("Store", "default")); 69 | headers2.add(new BasicHeader("Authentication", "Bearer 1234")); 70 | opt2.withHeaders(headers2); 71 | 72 | Assert.assertEquals(opt1.hashCode(), opt2.hashCode()); 73 | Assert.assertTrue(opt1.equals(opt2)); 74 | 75 | Assert.assertEquals(opt1.hashCode(), opt1.hashCode()); 76 | Assert.assertTrue(opt1.equals(opt1)); 77 | } 78 | 79 | @Test 80 | public void testNotEqualsDifferentHttpMethods() { 81 | RequestOptions opt1 = new RequestOptions(); 82 | opt1.withHttpMethod(HttpMethod.GET); 83 | 84 | RequestOptions opt2 = new RequestOptions(); 85 | opt2.withHttpMethod(HttpMethod.POST); 86 | 87 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 88 | Assert.assertFalse(opt1.equals(opt2)); 89 | } 90 | 91 | @Test 92 | public void testNotEqualsNoHttpMethodInOneRequestOptions() { 93 | RequestOptions opt1 = new RequestOptions(); 94 | opt1.withHttpMethod(HttpMethod.GET); 95 | 96 | RequestOptions opt2 = new RequestOptions(); // No httpMethod 97 | 98 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 99 | Assert.assertFalse(opt1.equals(opt2)); 100 | } 101 | 102 | @Test 103 | public void testNotEqualsNoHeadersInOneRequestOptions() { 104 | RequestOptions opt1 = new RequestOptions(); 105 | opt1.withHttpMethod(HttpMethod.GET); 106 | List
headers1 = new ArrayList<>(); 107 | headers1.add(new BasicHeader("Store", "default")); 108 | headers1.add(new BasicHeader("Authentication", "Bearer 1234")); 109 | opt1.withHeaders(headers1); 110 | 111 | RequestOptions opt2 = new RequestOptions(); // No headers 112 | opt2.withHttpMethod(HttpMethod.GET); 113 | 114 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 115 | Assert.assertFalse(opt1.equals(opt2)); 116 | Assert.assertFalse(opt2.equals(opt1)); // test "reverse" XOR condition 117 | } 118 | 119 | @Test 120 | public void testNotEqualsEmptyHeadersInOneRequestOptions() { 121 | RequestOptions opt1 = new RequestOptions(); 122 | opt1.withHttpMethod(HttpMethod.GET); 123 | List
headers1 = new ArrayList<>(); 124 | headers1.add(new BasicHeader("Store", "default")); 125 | headers1.add(new BasicHeader("Authentication", "Bearer 1234")); 126 | opt1.withHeaders(headers1); 127 | 128 | RequestOptions opt2 = new RequestOptions(); 129 | opt2.withHttpMethod(HttpMethod.GET); 130 | opt2.withHeaders(Collections.emptyList()); // Empty headers 131 | 132 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 133 | Assert.assertFalse(opt1.equals(opt2)); 134 | Assert.assertFalse(opt2.equals(opt1)); // test "reverse" OR condition 135 | } 136 | 137 | @Test 138 | public void testNotEqualsDifferentHeadersListSize() { 139 | RequestOptions opt1 = new RequestOptions(); 140 | opt1.withHttpMethod(HttpMethod.GET); 141 | List
headers1 = new ArrayList<>(); 142 | headers1.add(new BasicHeader("Store", "default")); 143 | headers1.add(new BasicHeader("Authentication", "Bearer 1234")); 144 | opt1.withHeaders(headers1); 145 | 146 | RequestOptions opt2 = new RequestOptions(); 147 | opt2.withHttpMethod(HttpMethod.GET); 148 | List
headers2 = new LinkedList<>(); 149 | headers2.add(new BasicHeader("Store", "default")); 150 | opt2.withHeaders(headers2); 151 | 152 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 153 | Assert.assertFalse(opt1.equals(opt2)); 154 | } 155 | 156 | @Test 157 | public void testNotEqualsDifferentHeaderValues() { 158 | RequestOptions opt1 = new RequestOptions(); 159 | opt1.withHttpMethod(HttpMethod.GET); 160 | List
headers1 = new ArrayList<>(); 161 | headers1.add(new BasicHeader("Store", "default")); 162 | headers1.add(new BasicHeader("Authentication", "Bearer 1234")); 163 | opt1.withHeaders(headers1); 164 | 165 | RequestOptions opt2 = new RequestOptions(); 166 | opt2.withHttpMethod(HttpMethod.GET); 167 | List
headers2 = new LinkedList<>(); 168 | headers2.add(new BasicHeader("Store", "default")); 169 | headers2.add(new BasicHeader("Authentication", "Bearer 5678")); 170 | opt2.withHeaders(headers2); 171 | 172 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 173 | Assert.assertFalse(opt1.equals(opt2)); 174 | } 175 | 176 | @Test 177 | public void testNotEqualsDifferentHeaderNames() { 178 | RequestOptions opt1 = new RequestOptions(); 179 | opt1.withHttpMethod(HttpMethod.GET); 180 | List
headers1 = new ArrayList<>(); 181 | headers1.add(new BasicHeader("Stored", "default")); 182 | headers1.add(new BasicHeader("Authentication", "Bearer 1234")); 183 | opt1.withHeaders(headers1); 184 | 185 | RequestOptions opt2 = new RequestOptions(); 186 | opt2.withHttpMethod(HttpMethod.GET); 187 | List
headers2 = new LinkedList<>(); 188 | headers2.add(new BasicHeader("Store", "default")); 189 | headers2.add(new BasicHeader("Authentication", "Bearer 1234")); 190 | opt2.withHeaders(headers2); 191 | 192 | Assert.assertNotEquals(opt1.hashCode(), opt2.hashCode()); 193 | Assert.assertFalse(opt1.equals(opt2)); 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/CacheInvalidatorTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2024 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.lang.reflect.Field; 18 | import java.lang.reflect.InvocationTargetException; 19 | import java.lang.reflect.Method; 20 | import java.lang.reflect.Modifier; 21 | import java.util.*; 22 | 23 | import org.apache.http.Header; 24 | import org.apache.http.message.BasicHeader; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | import org.mockito.Mock; 28 | import org.mockito.MockitoAnnotations; 29 | import org.slf4j.Logger; 30 | 31 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 32 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 33 | import com.adobe.cq.commerce.graphql.client.RequestOptions; 34 | import com.google.common.cache.Cache; 35 | import com.google.common.cache.CacheBuilder; 36 | 37 | import static org.junit.Assert.*; 38 | import static org.mockito.Mockito.times; 39 | import static org.mockito.Mockito.verify; 40 | 41 | public class CacheInvalidatorTest { 42 | 43 | private CacheInvalidator cacheInvalidator; 44 | 45 | private Map>> caches; 46 | 47 | private Map initialCounts; 48 | 49 | private Method checkIfStorePresentMethod; 50 | 51 | @Mock 52 | private Logger logger; 53 | 54 | private static class Data { 55 | String text; 56 | } 57 | 58 | private static class Error { 59 | String message; 60 | } 61 | 62 | @Before 63 | public void setUp() throws NoSuchMethodException { 64 | MockitoAnnotations.initMocks(this); 65 | 66 | caches = new HashMap<>(); 67 | 68 | // Create real caches and add them to the map 69 | Cache> cache1 = CacheBuilder.newBuilder().build(); 70 | Cache> cache2 = CacheBuilder.newBuilder().build(); 71 | 72 | // Add some data to the caches 73 | GraphqlRequest request1 = new GraphqlRequest("{dummy1}"); 74 | GraphqlRequest request2 = new GraphqlRequest("{dummy2}"); 75 | GraphqlRequest request3 = new GraphqlRequest("{dummy3}"); 76 | List
headers1 = Collections.singletonList(new BasicHeader("Store", "default")); 77 | RequestOptions options1 = new RequestOptions().withHeaders(headers1); 78 | List
headers2 = Collections.singletonList(new BasicHeader("Store", "defaultTest")); 79 | RequestOptions options2 = new RequestOptions().withHeaders(headers2); 80 | List
headersWithDifferentHeaders = Collections.singletonList(new BasicHeader("test", "default")); 81 | RequestOptions optionsWithDifferentHeaders = new RequestOptions().withHeaders(headersWithDifferentHeaders); 82 | RequestOptions optionsWithNoHeader = new RequestOptions(); 83 | CacheKey cacheKey1 = new CacheKey(request1, options1); 84 | CacheKey cacheKey2 = new CacheKey(request2, options2); 85 | CacheKey cacheKey3 = new CacheKey(request3, options1); 86 | CacheKey cacheKeyWithDifferentHeader = new CacheKey(request1, optionsWithDifferentHeaders); 87 | CacheKey cacheKeyWithNoHeader = new CacheKey(request1, optionsWithNoHeader); 88 | 89 | // Define the responses 90 | GraphqlResponse response1 = new GraphqlResponse<>(); 91 | response1.setData(new Data()); 92 | response1.getData().text = "sku1"; 93 | 94 | GraphqlResponse response2 = new GraphqlResponse<>(); 95 | response2.setData(new Data()); 96 | response2.getData().text = "sku2"; 97 | 98 | cache1.put(cacheKey1, response1); 99 | cache1.put(cacheKey2, response2); 100 | cache1.put(cacheKey3, response2); 101 | cache1.put(cacheKeyWithNoHeader, response1); 102 | cache1.put(cacheKeyWithDifferentHeader, response1); 103 | cache1.put(cacheKey3, response2); 104 | cache2.put(cacheKey2, response2); 105 | cache2.put(cacheKey1, response1); 106 | 107 | caches.put("cache1", cache1); 108 | caches.put("cache2", cache2); 109 | cacheInvalidator = new CacheInvalidator(caches); 110 | 111 | // Get the private method using reflection 112 | checkIfStorePresentMethod = CacheInvalidator.class.getDeclaredMethod("checkIfStorePresent", String.class, CacheKey.class); 113 | checkIfStorePresentMethod.setAccessible(true); 114 | 115 | // Store the initial count of entries in each cache 116 | initialCounts = new HashMap<>(); 117 | for (Map.Entry>> entry : caches.entrySet()) { 118 | initialCounts.put(entry.getKey(), entry.getValue().asMap().size()); 119 | } 120 | 121 | // Set the logger field to the mock logger 122 | setLoggerField(); 123 | 124 | } 125 | 126 | @Test 127 | public void testInvalidateAll() { 128 | // Call the invalidateCache method with parameters that trigger invalidateAll 129 | cacheInvalidator.invalidateCache(null, null, null); 130 | 131 | // Verify the caches were invalidated 132 | assertCachesInvalidated(); 133 | } 134 | 135 | @Test 136 | public void testInvalidateAllWithEmptyStoreView() { 137 | cacheInvalidator.invalidateCache("", null, null); 138 | assertCachesInvalidated(); 139 | } 140 | 141 | @Test 142 | public void testInvalidateAllWithEmptyArray() { 143 | // Call the invalidateCache method with parameters that trigger invalidateAll 144 | cacheInvalidator.invalidateCache(null, new String[] {}, new String[] {}); 145 | 146 | // Verify the caches were invalidated 147 | assertCachesInvalidated(); 148 | } 149 | 150 | @Test 151 | public void testInvalidateStoreView() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { 152 | cacheInvalidator.invalidateCache("default", null, null); 153 | // Verify that the cache was invalidated for the store view, were it has been set as default 154 | assertInvalidateStoreView(); 155 | } 156 | 157 | @Test 158 | public void testInvalidateStoreViewWithEmptyArray() throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { 159 | cacheInvalidator.invalidateCache("default", new String[] {}, new String[] {}); 160 | // Verify that the cache was invalidated for the store view, were it has been set as default 161 | assertInvalidateStoreView(); 162 | } 163 | 164 | @Test 165 | public void testInvalidateCacheBasedOnSpecificCacheName() { 166 | cacheInvalidator.invalidateCache(null, new String[] { "cache1" }, null); 167 | assertInvalidateSpecificCaches("cache1"); 168 | } 169 | 170 | @Test 171 | public void testInvalidateCacheBasedOnSpecificCacheNameWithEmptyArray() { 172 | cacheInvalidator.invalidateCache(null, new String[] { "cache1" }, new String[] {}); 173 | assertInvalidateSpecificCaches("cache1"); 174 | } 175 | 176 | @Test 177 | public void testInvalidateCacheWithMultipleCacheNames() { 178 | // This will clear cache1 and cache2 i.e all the caches 179 | cacheInvalidator.invalidateCache(null, new String[] { "cache1", "cache2" }, null); 180 | assertInvalidateSpecificCaches("cache1", "cache2"); 181 | } 182 | 183 | @Test 184 | public void testInvalidateCacheWithInvalidCacheNames() { 185 | // This will not clear any cache as the cache names are invalid 186 | cacheInvalidator.invalidateCache(null, new String[] { "cachetest1" }, null); 187 | 188 | // Verify that the count of entries in each cache is the same as before 189 | verifyCacheSizes(); 190 | } 191 | 192 | @Test 193 | public void testInvalidateCacheWithCacheNameAndStoreView() throws InvocationTargetException, IllegalAccessException { 194 | assertCacheInvalidation("default", new String[] { "cache1" }, null, null); 195 | } 196 | 197 | @Test 198 | public void testInvalidateCacheInMultipleCacheListForSpecificPattern() throws InvocationTargetException, IllegalAccessException { 199 | assertCacheInvalidation("default", null, new String[] { "\"text\":\\s*\"(sku2)\"" }, "sku2"); 200 | } 201 | 202 | @Test 203 | public void testInvalidateCacheForEmptySpecificPattern() { 204 | cacheInvalidator.invalidateCache("defaultTest", null, new String[] { null, "" }); 205 | verify(logger, times(2)).debug("Skipping null pattern in patterns array"); 206 | 207 | } 208 | 209 | @Test 210 | public void testInvalidateCache_WithNonExistingCacheListForSpecificPattern() { 211 | cacheInvalidator.invalidateCache("default", new String[] { "samplecache", "samplecache2", "", null }, new String[] { 212 | "\"text\":\\s*\"(sku2)\"" }); 213 | verifyCacheSizes(); 214 | } 215 | 216 | @Test 217 | public void testInvalidateCachePattern_EmptyCacheNames() throws InvocationTargetException, IllegalAccessException { 218 | assertCacheInvalidation("defaultTest", new String[] {}, new String[] { "\"text\":\\s*\"(sku2)\"" }, "sku2"); 219 | } 220 | 221 | @Test 222 | public void testInvalidateCacheWithStoreViewDefaultTestAndCacheNameCache2AndTextSku2() throws InvocationTargetException, 223 | IllegalAccessException { 224 | assertCacheInvalidation("defaultTest", new String[] { "cache2" }, new String[] { "\"text\":\\s*\"(sku2)\"" }, "sku2"); 225 | } 226 | 227 | @Test 228 | public void testInvalidateCacheWithStoreViewDefaultTestAndMultipleStringPatterns() throws InvocationTargetException, 229 | IllegalAccessException { 230 | assertCacheInvalidation("default", null, new String[] { "\"text\":\\s*\"(sku2)\"", "\"text\":\\s*\"(sku1)\"" }, "sku2", "sku1"); 231 | } 232 | 233 | private void assertCacheInvalidation(String storeView, String[] cacheNames, String[] patterns, String... expectedTexts) 234 | throws InvocationTargetException, IllegalAccessException { 235 | cacheInvalidator.invalidateCache(storeView, cacheNames, patterns); 236 | 237 | for (Map.Entry>> cacheEntry : caches.entrySet()) { 238 | Cache> cache = cacheEntry.getValue(); 239 | String cacheName = cacheEntry.getKey(); 240 | for (Map.Entry> entry : cache.asMap().entrySet()) { 241 | CacheKey key = entry.getKey(); 242 | GraphqlResponse response = entry.getValue(); 243 | 244 | boolean storeViewMatches = storeView == null || checkIfStorePresent(storeView, key); 245 | boolean cacheNameMatches = cacheNames == null || Arrays.asList(cacheNames).contains(cacheName); 246 | boolean textMatches = expectedTexts == null || (response.getData() != null && Arrays.stream(expectedTexts).anyMatch( 247 | text -> text.equals(((Data) response.getData()).text))); 248 | 249 | assertFalse("Cache with specified criteria found", storeViewMatches && cacheNameMatches && textMatches); 250 | } 251 | } 252 | } 253 | 254 | @Test 255 | public void testInvalidateCacheWithStoreViewDefaultTestAndComplexStringPattern() throws InvocationTargetException, 256 | IllegalAccessException { 257 | assertCacheInvalidation("default", null, new String[] { "\"text\":\\s*\"(sku2|sku1)\"" }, "sku2", "sku1"); 258 | } 259 | 260 | @Test 261 | public void testCheckIfStorePresentWithNullOrEmptyParameters() throws InvocationTargetException, IllegalAccessException { 262 | // Create a valid cache key for comparison 263 | GraphqlRequest request = new GraphqlRequest("{test}"); 264 | List
headers = Collections.singletonList(new BasicHeader("Store", "default")); 265 | RequestOptions options = new RequestOptions().withHeaders(headers); 266 | CacheKey validCacheKey = new CacheKey(request, options); 267 | 268 | // Test with null storeView 269 | assertFalse("Should return false when storeView is null", 270 | checkIfStorePresent(null, validCacheKey)); 271 | 272 | // Test with null cacheKey 273 | assertFalse("Should return false when cacheKey is null", 274 | checkIfStorePresent("default", null)); 275 | 276 | // Test with null requestOptions 277 | CacheKey cacheKeyWithNullOptions = new CacheKey(request, null); 278 | assertFalse("Should return false when requestOptions is null", 279 | checkIfStorePresent("default", cacheKeyWithNullOptions)); 280 | 281 | // Test with null headers (no headers provided) 282 | CacheKey cacheKeyWithNoHeaders = new CacheKey(request, new RequestOptions()); 283 | assertFalse("Should return false when headers are null", 284 | checkIfStorePresent("default", cacheKeyWithNoHeaders)); 285 | 286 | // Test with empty headers list 287 | CacheKey cacheKeyWithEmptyHeaders = new CacheKey(request, new RequestOptions().withHeaders(Collections.emptyList())); 288 | assertFalse("Should return false when headers list is empty", 289 | checkIfStorePresent("default", cacheKeyWithEmptyHeaders)); 290 | } 291 | 292 | @Test 293 | public void testIsAllParametersEmpty() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 294 | // Get access to the private method using reflection 295 | Method isAllParametersEmptyMethod = CacheInvalidator.class.getDeclaredMethod( 296 | "isAllParametersEmpty", String.class, String[].class, String[].class); 297 | isAllParametersEmptyMethod.setAccessible(true); 298 | 299 | // Test with null values for all parameters 300 | boolean result1 = (boolean) isAllParametersEmptyMethod.invoke(cacheInvalidator, null, null, null); 301 | assertTrue("Should return true when all parameters are null", result1); 302 | 303 | // Test with empty string and empty arrays 304 | boolean result2 = (boolean) isAllParametersEmptyMethod.invoke(cacheInvalidator, "", new String[] {}, new String[] {}); 305 | assertTrue("Should return true when all parameters are empty", result2); 306 | 307 | // Test with non-empty storeView 308 | boolean result3 = (boolean) isAllParametersEmptyMethod.invoke(cacheInvalidator, "default", null, null); 309 | assertFalse("Should return false when storeView is not empty", result3); 310 | 311 | // Test with non-empty cacheNames 312 | boolean result4 = (boolean) isAllParametersEmptyMethod.invoke(cacheInvalidator, "", new String[] { "cache1" }, null); 313 | assertFalse("Should return false when cacheNames is not empty", result4); 314 | 315 | // Test with non-empty patterns 316 | boolean result5 = (boolean) isAllParametersEmptyMethod.invoke(cacheInvalidator, "", null, new String[] { "pattern" }); 317 | assertFalse("Should return false when patterns is not empty", result5); 318 | 319 | // Test with whitespace storeView (should be considered blank) 320 | boolean result6 = (boolean) isAllParametersEmptyMethod.invoke(cacheInvalidator, " ", new String[] {}, new String[] {}); 321 | assertTrue("Should return true when storeView only contains whitespace", result6); 322 | } 323 | 324 | @Test 325 | public void testIsCacheNamesProvided() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException { 326 | // Get access to the private method using reflection 327 | Method isCacheNamesProvidedMethod = CacheInvalidator.class.getDeclaredMethod( 328 | "isCacheNamesProvided", String[].class, String[].class); 329 | isCacheNamesProvidedMethod.setAccessible(true); 330 | 331 | // Test with non-empty cacheNames and null patterns 332 | boolean result1 = (boolean) isCacheNamesProvidedMethod.invoke(cacheInvalidator, new String[] { "cache1" }, null); 333 | assertTrue("Should return true when cacheNames is not empty and patterns is null", result1); 334 | 335 | // Test with non-empty cacheNames and empty patterns 336 | boolean result2 = (boolean) isCacheNamesProvidedMethod.invoke(cacheInvalidator, new String[] { "cache1" }, new String[] {}); 337 | assertTrue("Should return true when cacheNames is not empty and patterns is empty", result2); 338 | 339 | // Test with null cacheNames 340 | boolean result3 = (boolean) isCacheNamesProvidedMethod.invoke(cacheInvalidator, null, null); 341 | assertFalse("Should return false when cacheNames is null", result3); 342 | 343 | // Test with empty cacheNames 344 | boolean result4 = (boolean) isCacheNamesProvidedMethod.invoke(cacheInvalidator, new String[] {}, null); 345 | assertFalse("Should return false when cacheNames is empty", result4); 346 | 347 | // Test with non-empty patterns 348 | boolean result5 = (boolean) isCacheNamesProvidedMethod.invoke(cacheInvalidator, new String[] { "cache1" }, new String[] { 349 | "pattern" }); 350 | assertFalse("Should return false when patterns is not empty", result5); 351 | } 352 | 353 | private void assertCachesInvalidated() { 354 | for (Cache> cache : caches.values()) { 355 | assertTrue(cache.asMap().isEmpty()); 356 | } 357 | } 358 | 359 | private void assertInvalidateStoreView() throws InvocationTargetException, IllegalAccessException { 360 | for (Cache> cache : caches.values()) { 361 | for (CacheKey key : cache.asMap().keySet()) { 362 | boolean storeViewExists = checkIfStorePresent("default", key); 363 | assertFalse("Store view default not found in headers", storeViewExists); 364 | } 365 | } 366 | } 367 | 368 | private void assertInvalidateSpecificCaches(String... cacheNames) { 369 | // Verify that other caches are not empty 370 | Set cacheNamesSet = new HashSet<>(Arrays.asList(cacheNames)); 371 | for (Map.Entry>> entry : caches.entrySet()) { 372 | if (cacheNamesSet.contains(entry.getKey())) { 373 | assertTrue(entry.getValue().asMap().isEmpty()); 374 | } else { 375 | assertFalse(entry.getValue().asMap().isEmpty()); 376 | } 377 | } 378 | } 379 | 380 | private boolean checkIfStorePresent(String storeView, CacheKey cacheKey) throws InvocationTargetException, IllegalAccessException { 381 | return (boolean) checkIfStorePresentMethod.invoke(cacheInvalidator, storeView, cacheKey); 382 | } 383 | 384 | private void verifyCacheSizes() { 385 | // Verify that the count of entries in each cache is the same as before 386 | for (Map.Entry>> entry : caches.entrySet()) { 387 | assertEquals(initialCounts.get(entry.getKey()).intValue(), entry.getValue().asMap().size()); 388 | } 389 | } 390 | 391 | private void setLoggerField() { 392 | try { 393 | Field loggerField = CacheInvalidator.class.getDeclaredField("LOGGER"); 394 | loggerField.setAccessible(true); 395 | 396 | Field modifiersField = Field.class.getDeclaredField("modifiers"); 397 | modifiersField.setAccessible(true); 398 | modifiersField.setInt(loggerField, loggerField.getModifiers() & ~Modifier.FINAL); 399 | loggerField.set(null, logger); 400 | } catch (NoSuchFieldException | IllegalAccessException e) { 401 | throw new RuntimeException("Failed to set logger field", e); 402 | } 403 | } 404 | } 405 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/CacheKeyTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import org.junit.Assert; 18 | import org.junit.Test; 19 | 20 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 21 | import com.adobe.cq.commerce.graphql.client.HttpMethod; 22 | import com.adobe.cq.commerce.graphql.client.RequestOptions; 23 | 24 | public class CacheKeyTest { 25 | 26 | @Test 27 | public void testEquals() { 28 | GraphqlRequest req1 = new GraphqlRequest("{dummy}"); 29 | RequestOptions opt1 = new RequestOptions(); 30 | CacheKey key1 = new CacheKey(req1, opt1); 31 | 32 | GraphqlRequest req2 = new GraphqlRequest("{dummy}"); 33 | RequestOptions opt2 = new RequestOptions(); 34 | CacheKey key2 = new CacheKey(req2, opt2); 35 | 36 | Assert.assertEquals(key1.hashCode(), key2.hashCode()); 37 | Assert.assertTrue(key1.equals(key2)); 38 | 39 | Assert.assertEquals(key1.hashCode(), key1.hashCode()); 40 | Assert.assertTrue(key1.equals(key1)); 41 | Assert.assertFalse(key1.equals("wrongclass")); 42 | Assert.assertFalse(key1.equals(null)); 43 | } 44 | 45 | @Test 46 | public void testNotEqualsDifferentRequest() { 47 | GraphqlRequest req1 = new GraphqlRequest("{dummy}"); 48 | RequestOptions opt1 = new RequestOptions(); 49 | CacheKey key1 = new CacheKey(req1, opt1); 50 | 51 | GraphqlRequest req2 = new GraphqlRequest("{somethingelse}"); 52 | RequestOptions opt2 = new RequestOptions(); 53 | CacheKey key2 = new CacheKey(req2, opt2); 54 | 55 | Assert.assertNotEquals(key1.hashCode(), key2.hashCode()); 56 | Assert.assertFalse(key1.equals(key2)); 57 | } 58 | 59 | @Test 60 | public void testNotEqualsDifferentOptions() { 61 | GraphqlRequest req1 = new GraphqlRequest("{dummy}"); 62 | RequestOptions opt1 = new RequestOptions().withHttpMethod(HttpMethod.GET); 63 | CacheKey key1 = new CacheKey(req1, opt1); 64 | 65 | GraphqlRequest req2 = new GraphqlRequest("{dummy}"); 66 | RequestOptions opt2 = new RequestOptions().withHttpMethod(HttpMethod.POST); 67 | CacheKey key2 = new CacheKey(req2, opt2); 68 | 69 | Assert.assertNotEquals(key1.hashCode(), key2.hashCode()); 70 | Assert.assertFalse(key1.equals(key2)); 71 | } 72 | 73 | @Test 74 | public void testNotEqualsNoOption() { 75 | GraphqlRequest req1 = new GraphqlRequest("{dummy}"); 76 | RequestOptions opt1 = new RequestOptions(); 77 | CacheKey key1 = new CacheKey(req1, opt1); 78 | 79 | GraphqlRequest req2 = new GraphqlRequest("{dummy}"); 80 | CacheKey key2 = new CacheKey(req2, null); 81 | 82 | Assert.assertNotEquals(key1.hashCode(), key2.hashCode()); 83 | Assert.assertFalse(key1.equals(key2)); 84 | } 85 | 86 | @Test 87 | public void testGetGraphqlRequest() { 88 | GraphqlRequest req = new GraphqlRequest("{dummy}"); 89 | RequestOptions opt = new RequestOptions(); 90 | CacheKey key = new CacheKey(req, opt); 91 | 92 | Assert.assertEquals(req, key.getGraphqlRequest()); 93 | } 94 | 95 | @Test 96 | public void testGetRequestOptions() { 97 | GraphqlRequest req = new GraphqlRequest("{dummy}"); 98 | RequestOptions opt = new RequestOptions(); 99 | CacheKey key = new CacheKey(req, opt); 100 | 101 | Assert.assertEquals(opt, key.getRequestOptions()); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/ConcurrencyTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.io.IOException; 18 | import java.util.ArrayList; 19 | import java.util.Collection; 20 | import java.util.concurrent.ExecutorService; 21 | import java.util.concurrent.Executors; 22 | import java.util.concurrent.Future; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import org.apache.http.HttpHeaders; 26 | import org.junit.AfterClass; 27 | import org.junit.Before; 28 | import org.junit.BeforeClass; 29 | import org.junit.Test; 30 | import org.mockserver.model.HttpRequest; 31 | import org.mockserver.model.HttpResponse; 32 | import org.osgi.framework.BundleContext; 33 | 34 | import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration; 35 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 36 | import com.adobe.cq.commerce.graphql.client.impl.MockServerHelper.Data; 37 | import com.adobe.cq.commerce.graphql.client.impl.MockServerHelper.Error; 38 | 39 | import static org.junit.Assert.assertEquals; 40 | import static org.mockito.Mockito.mock; 41 | import static org.mockserver.model.HttpStatusCode.INTERNAL_SERVER_ERROR_500; 42 | 43 | public class ConcurrencyTest { 44 | 45 | private static final int THREAD_COUNT = GraphqlClientConfiguration.MAX_HTTP_CONNECTIONS_DEFAULT * 2; 46 | 47 | private static MockServerHelper mockServer; 48 | 49 | private GraphqlClientImpl graphqlClient; 50 | 51 | @BeforeClass 52 | public static void initServer() throws IOException { 53 | mockServer = new MockServerHelper(); 54 | } 55 | 56 | @AfterClass 57 | public static void stopServer() { 58 | mockServer.stop(); 59 | } 60 | 61 | @Before 62 | public void setUp() throws Exception { 63 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 64 | config.setUrl("https://localhost:" + mockServer.getLocalPort() + "/graphql"); 65 | config.setAcceptSelfSignedCertificates(true); 66 | 67 | graphqlClient = new GraphqlClientImpl(); 68 | graphqlClient.activate(config, mock(BundleContext.class)); 69 | } 70 | 71 | @Test(timeout = 15000) 72 | public void testConcurrencyWithResponses() throws Exception { 73 | mockServer.resetWithSampleResponse(); 74 | 75 | ExecutorService service = Executors.newFixedThreadPool(THREAD_COUNT); 76 | Collection>> futures = new ArrayList<>(THREAD_COUNT); 77 | for (int t = 0; t < THREAD_COUNT; t++) { 78 | futures.add(service.submit(() -> mockServer.executeGraphqlClientDummyRequest(graphqlClient))); 79 | } 80 | 81 | Collection> responses = new ArrayList<>(); 82 | for (Future> f : futures) { 83 | responses.add(f.get()); 84 | } 85 | 86 | assertEquals(THREAD_COUNT, responses.size()); 87 | for (GraphqlResponse response : responses) { 88 | mockServer.validateSampleResponse(response); 89 | } 90 | } 91 | 92 | @Test(timeout = 15000) 93 | public void testConcurrencyWithErrors() throws Exception { 94 | 95 | // If the error messages are not consumed by the client, this test will time out 96 | // because all threads from the connection pool will stay stuck 97 | 98 | HttpResponse error = new HttpResponse().withStatusCode(INTERNAL_SERVER_ERROR_500.code()) 99 | .withReasonPhrase(INTERNAL_SERVER_ERROR_500.reasonPhrase()) 100 | .withHeader(HttpHeaders.CONNECTION, "keep-alive") 101 | .withBody("Dummy content that MUST be consumed by the client").withDelay(TimeUnit.MILLISECONDS, 50); 102 | 103 | mockServer.reset().when(HttpRequest.request().withPath("/graphql")).respond(error); 104 | 105 | ExecutorService service = Executors.newFixedThreadPool(THREAD_COUNT); 106 | Collection>> futures = new ArrayList<>(THREAD_COUNT); 107 | for (int t = 0; t < THREAD_COUNT; t++) { 108 | futures.add(service.submit(() -> { 109 | try { 110 | return mockServer.executeGraphqlClientDummyRequest(graphqlClient); 111 | } catch (Exception e) { 112 | return null; 113 | } 114 | })); 115 | } 116 | 117 | Collection> responses = new ArrayList<>(); 118 | for (Future> f : futures) { 119 | responses.add(f.get()); 120 | } 121 | assertEquals(THREAD_COUNT, responses.size()); 122 | } 123 | 124 | @Test(timeout = 15000) 125 | public void testConcurrencyWithTimeout() throws Exception { 126 | 127 | // The timeout of the HTTP client is much smaller than the timeout we define in the mock server 128 | // so we expect that all connection attemps will properly time out before the test itself times out 129 | 130 | HttpResponse error = new HttpResponse().withStatusCode(INTERNAL_SERVER_ERROR_500.code()) 131 | .withReasonPhrase(INTERNAL_SERVER_ERROR_500.reasonPhrase()) 132 | .withHeader(HttpHeaders.CONNECTION, "keep-alive").withDelay(TimeUnit.SECONDS, 60); // Must be higher 133 | // than test timeout 134 | 135 | mockServer.reset().when(HttpRequest.request().withPath("/graphql")).respond(error); 136 | 137 | ExecutorService service = Executors.newFixedThreadPool(THREAD_COUNT); 138 | Collection>> futures = new ArrayList<>(THREAD_COUNT); 139 | for (int t = 0; t < THREAD_COUNT; t++) { 140 | futures.add(service.submit(() -> { 141 | try { 142 | return mockServer.executeGraphqlClientDummyRequest(graphqlClient); 143 | } catch (Exception e) { 144 | return null; 145 | } 146 | })); 147 | } 148 | 149 | Collection> responses = new ArrayList<>(); 150 | for (Future> f : futures) { 151 | responses.add(f.get()); 152 | } 153 | assertEquals(THREAD_COUNT, responses.size()); 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlAemContext.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.util.Collections; 18 | import java.util.Dictionary; 19 | import java.util.Hashtable; 20 | import java.util.Map; 21 | 22 | import org.apache.sling.testing.mock.sling.ResourceResolverType; 23 | import org.osgi.service.cm.Configuration; 24 | import org.osgi.service.cm.ConfigurationAdmin; 25 | 26 | import com.adobe.cq.commerce.graphql.client.GraphqlClient; 27 | import io.wcm.testing.mock.aem.junit.AemContext; 28 | import io.wcm.testing.mock.aem.junit.AemContextBuilder; 29 | 30 | import static org.apache.sling.testing.mock.caconfig.ContextPlugins.CACONFIG; 31 | import static org.mockito.Mockito.mock; 32 | import static org.mockito.Mockito.when; 33 | 34 | public final class GraphqlAemContext { 35 | 36 | public final static String CATALOG_IDENTIFIER = "my-catalog"; 37 | 38 | public static GraphqlClientAdapterFactory adapterFactory; 39 | 40 | private GraphqlAemContext() {} 41 | 42 | public static AemContext createContext() { 43 | return createContext(Collections.emptyMap()); 44 | } 45 | 46 | public static AemContext createContext(Map contentPaths) { 47 | GraphqlAemContext.adapterFactory = new GraphqlClientAdapterFactory(); 48 | 49 | AemContext ctx = new AemContextBuilder(ResourceResolverType.JCR_MOCK).plugin(CACONFIG) 50 | .beforeSetUp(context -> { 51 | ConfigurationAdmin configurationAdmin = context.getService(ConfigurationAdmin.class); 52 | Configuration serviceConfiguration = configurationAdmin.getConfiguration( 53 | "org.apache.sling.caconfig.resource.impl.def.DefaultContextPathStrategy"); 54 | 55 | Dictionary props = new Hashtable<>(); 56 | props.put("configRefResourceNames", new String[] { ".", "jcr:content" }); 57 | props.put("configRefPropertyNames", "cq:conf"); 58 | serviceConfiguration.update(props); 59 | 60 | serviceConfiguration = configurationAdmin.getConfiguration( 61 | "org.apache.sling.caconfig.resource.impl.def.DefaultConfigurationResourceResolvingStrategy"); 62 | props = new Hashtable<>(); 63 | props.put("configPath", "/conf"); 64 | serviceConfiguration.update(props); 65 | 66 | serviceConfiguration = configurationAdmin.getConfiguration("org.apache.sling.caconfig.impl.ConfigurationResolverImpl"); 67 | props = new Hashtable<>(); 68 | props.put("configBucketNames", new String[] { "settings" }); 69 | serviceConfiguration.update(props); 70 | }).build(); 71 | GraphqlClient mockClient = mock(GraphqlClient.class); 72 | when(mockClient.getIdentifier()).thenReturn(CATALOG_IDENTIFIER); 73 | ctx.registerService(GraphqlClient.class, mockClient, GraphqlClientImpl.PROP_IDENTIFIER, CATALOG_IDENTIFIER); 74 | 75 | // Add AdapterFactory 76 | ctx.registerInjectActivateService(GraphqlAemContext.adapterFactory); 77 | 78 | // Load page structure 79 | contentPaths.entrySet().iterator().forEachRemaining(entry -> { 80 | ctx.load().json(entry.getValue(), entry.getKey()); 81 | }); 82 | 83 | return ctx; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientAdapterFactoryTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import org.apache.sling.api.resource.Resource; 18 | import org.junit.Assert; 19 | import org.junit.Rule; 20 | import org.junit.Test; 21 | import org.osgi.framework.BundleContext; 22 | import org.osgi.framework.Constants; 23 | 24 | import com.adobe.cq.commerce.graphql.client.GraphqlClient; 25 | import com.google.common.collect.ImmutableMap; 26 | import io.wcm.testing.mock.aem.junit.AemContext; 27 | 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertNotEquals; 30 | import static org.junit.Assert.assertNotNull; 31 | import static org.mockito.Mockito.mock; 32 | import static org.mockito.Mockito.when; 33 | 34 | public class GraphqlClientAdapterFactoryTest { 35 | 36 | @Rule 37 | public final AemContext context = GraphqlAemContext.createContext(ImmutableMap.of( 38 | "/content", "/context/graphql-client-adapter-factory-context.json")); 39 | 40 | @Test 41 | public void testGetClientForPageWithIdentifier() { 42 | // Get page which has the catalog identifier in its jcr:content node 43 | Resource res = context.resourceResolver().getResource("/content/pageA"); 44 | 45 | // Adapt page to client, verify that correct client was returned 46 | GraphqlClient client = res.adaptTo(GraphqlClient.class); 47 | Assert.assertNotNull(client); 48 | Assert.assertEquals(GraphqlAemContext.CATALOG_IDENTIFIER, client.getIdentifier()); 49 | } 50 | 51 | @Test 52 | public void testGetClientForPageWithInheritedIdentifier() { 53 | // Get page whose parent has the catalog identifier in its jcr:content node 54 | Resource res = context.resourceResolver().getResource("/content/pageB/pageC"); 55 | 56 | // Adapt page to client, verify that correct client was returned 57 | GraphqlClient client = res.adaptTo(GraphqlClient.class); 58 | Assert.assertNotNull(client); 59 | Assert.assertEquals(GraphqlAemContext.CATALOG_IDENTIFIER, client.getIdentifier()); 60 | } 61 | 62 | @Test 63 | public void testReturnNullForPageWithoutIdentifier() { 64 | // Get page whose parent has the catalog identifier in its jcr:content node 65 | Resource res = context.resourceResolver().getResource("/content/pageD"); 66 | 67 | // Adapt page to client, verify that no client can be returned 68 | GraphqlClient client = res.adaptTo(GraphqlClient.class); 69 | Assert.assertNull(client); 70 | } 71 | 72 | @Test 73 | public void testReturnNullForNotExistingResolver() { 74 | // Remove mockClient from resolver 75 | GraphqlClient client = context.getService(GraphqlClient.class); 76 | GraphqlAemContext.adapterFactory.unbindGraphqlClient(client); 77 | Assert.assertEquals(0, GraphqlAemContext.adapterFactory.clients.size()); 78 | 79 | // Get page which has the catalog identifier in its jcr:content node 80 | Resource res = context.resourceResolver().getResource("/content/pageA"); 81 | 82 | // Adapt page to client, verify that no client can be returned 83 | client = res.adaptTo(GraphqlClient.class); 84 | Assert.assertNull(client); 85 | } 86 | 87 | @Test 88 | public void testMultipleClientsWithServiceRanking() { 89 | GraphqlClient additionalClient = mock(GraphqlClient.class); 90 | when(additionalClient.getIdentifier()).thenReturn(GraphqlAemContext.CATALOG_IDENTIFIER); 91 | 92 | // add a client with a high ranking that should not be used / never returned 93 | GraphqlClient unusedClient = mock(GraphqlClient.class); 94 | when(unusedClient.getIdentifier()).thenReturn("unused"); 95 | context.registerService(GraphqlClient.class, unusedClient, Constants.SERVICE_RANKING, 1000); 96 | 97 | context.registerService(GraphqlClient.class, additionalClient, Constants.SERVICE_RANKING, -10); 98 | 99 | // should get the default gql client (service ranking = 0) 100 | GraphqlClient client = context.resourceResolver().getResource("/content/pageA").adaptTo(GraphqlClient.class); 101 | assertNotEquals(additionalClient, client); 102 | 103 | context.registerService(GraphqlClient.class, additionalClient, Constants.SERVICE_RANKING, 10); 104 | 105 | // should get the additional gql client (service ranking = 10) 106 | client = context.resourceResolver().getResource("/content/pageA").adaptTo(GraphqlClient.class); 107 | assertEquals(additionalClient, client); 108 | } 109 | 110 | @Test 111 | public void testErrorCases() throws Exception { 112 | GraphqlClientImpl graphqlClient = new GraphqlClientImpl(); 113 | MockGraphqlClientConfiguration configuration = new MockGraphqlClientConfiguration(); 114 | configuration.setIdentifier("default"); 115 | graphqlClient.activate(configuration, mock(BundleContext.class)); 116 | 117 | GraphqlClientAdapterFactory factory = new GraphqlClientAdapterFactory(); 118 | factory.bindGraphqlClient(graphqlClient, ImmutableMap.of( 119 | Constants.SERVICE_ID, 999L, 120 | GraphqlClientImpl.PROP_IDENTIFIER, "default")); 121 | 122 | // Ensure that adapter returns null if not adapted from a resource 123 | Object target = factory.getAdapter(factory, Object.class); 124 | Assert.assertNull(target); 125 | 126 | // Ensure that adapter returns null if not adapting to a GraphQL client 127 | Resource res = context.resourceResolver().getResource("/content/test"); 128 | target = factory.getAdapter(res, Object.class); 129 | Assert.assertNull(target); 130 | 131 | // Ensure it works in the right case 132 | target = factory.getAdapter(res, GraphqlClient.class); 133 | Assert.assertNotNull(target); 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientConfigurationImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2022 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | package com.adobe.cq.commerce.graphql.client.impl; 15 | 16 | import org.junit.Test; 17 | 18 | import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration; 19 | 20 | import static org.junit.Assert.assertNotNull; 21 | import static org.mockito.Mockito.mock; 22 | import static org.mockito.Mockito.when; 23 | 24 | public class GraphqlClientConfigurationImplTest { 25 | 26 | @Test 27 | public void testReturnsAlwaysNonNullHeaders() { 28 | GraphqlClientConfiguration mockConfig = mock(GraphqlClientConfiguration.class); 29 | assertNotNull(new GraphqlClientConfigurationImpl(mockConfig).httpHeaders()); 30 | when(mockConfig.httpHeaders()).thenReturn(new String[] { "foo: bar" }); 31 | assertNotNull(new GraphqlClientConfigurationImpl(mockConfig).httpHeaders()); 32 | } 33 | 34 | @Test 35 | public void testReturnsAlwaysNonNullCacheConfigurations() { 36 | GraphqlClientConfiguration mockConfig = mock(GraphqlClientConfiguration.class); 37 | assertNotNull(new GraphqlClientConfigurationImpl(mockConfig).cacheConfigurations()); 38 | when(mockConfig.cacheConfigurations()).thenReturn(new String[] { "foo: bar" }); 39 | assertNotNull(new GraphqlClientConfigurationImpl(mockConfig).cacheConfigurations()); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientImplCachingTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.util.Collections; 18 | 19 | import org.apache.commons.lang3.ArrayUtils; 20 | import org.apache.http.HttpStatus; 21 | import org.apache.http.client.HttpClient; 22 | import org.apache.http.client.ResponseHandler; 23 | import org.apache.http.message.BasicHeader; 24 | import org.junit.Before; 25 | import org.junit.Test; 26 | import org.mockito.Mockito; 27 | import org.osgi.framework.BundleContext; 28 | 29 | import com.adobe.cq.commerce.graphql.client.CachingStrategy; 30 | import com.adobe.cq.commerce.graphql.client.CachingStrategy.DataFetchingPolicy; 31 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 32 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 33 | import com.adobe.cq.commerce.graphql.client.RequestOptions; 34 | 35 | import static org.junit.Assert.assertEquals; 36 | import static org.mockito.Mockito.mock; 37 | 38 | public class GraphqlClientImplCachingTest { 39 | 40 | private static class Data { 41 | String text; 42 | Integer count; 43 | } 44 | 45 | private static class Error { 46 | String message; 47 | } 48 | 49 | private GraphqlClientImpl graphqlClient; 50 | private GraphqlRequest dummy = new GraphqlRequest("{dummy}"); 51 | 52 | private static final String MY_CACHE = "mycache"; 53 | private static final String MY_DISABLED_CACHE = "mydisabledcache"; 54 | 55 | @Before 56 | public void setUp() throws Exception { 57 | graphqlClient = new GraphqlClientImpl(); 58 | 59 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 60 | config.setCacheConfigurations(MY_CACHE + ":true:100:5", MY_DISABLED_CACHE + ":false:100:5", ""); 61 | 62 | graphqlClient.activate(config, mock(BundleContext.class)); 63 | graphqlClient.client = mock(HttpClient.class); 64 | } 65 | 66 | @Test(expected = IllegalStateException.class) 67 | public void testInvalidCacheConfiguration() throws Exception { 68 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 69 | config.setCacheConfigurations(MY_CACHE + ":true:"); // Not enough parameters 70 | graphqlClient.activate(config, mock(BundleContext.class)); 71 | } 72 | 73 | @Test(expected = NumberFormatException.class) 74 | public void testInvalidMaxSizeParameter() throws Exception { 75 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 76 | config.setCacheConfigurations(MY_CACHE + ":true:bad:5"); // Cache max size must be an Integer 77 | graphqlClient.activate(config, mock(BundleContext.class)); 78 | } 79 | 80 | @Test(expected = NumberFormatException.class) 81 | public void testInvalidTimeoutParameter() throws Exception { 82 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 83 | config.setCacheConfigurations(MY_CACHE + ":true:100:bad"); // Cache timeout must be an Integer 84 | graphqlClient.activate(config, mock(BundleContext.class)); 85 | } 86 | 87 | @Test 88 | public void testActiveCache() throws Exception { 89 | CachingStrategy cachingStrategy = new CachingStrategy() 90 | .withCacheName(MY_CACHE) 91 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 92 | RequestOptions requestOptions = new RequestOptions() 93 | .withCachingStrategy(cachingStrategy); 94 | 95 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 96 | GraphqlResponse response = graphqlClient.execute(dummy, Data.class, Error.class, requestOptions); 97 | assertEquals("Some text", response.getData().text); 98 | assertEquals(42, response.getData().count.intValue()); 99 | 100 | // This reponse is coming from the cache 101 | GraphqlResponse response2 = graphqlClient.execute(dummy, Data.class, Error.class, requestOptions); 102 | assertEquals("Some text", response2.getData().text); 103 | assertEquals(42, response2.getData().count.intValue()); 104 | 105 | // HTTP client was only called once 106 | Mockito.verify(graphqlClient.client).execute(Mockito.any(), Mockito.any(ResponseHandler.class)); 107 | } 108 | 109 | @Test 110 | public void testActiveCacheButDifferentQueries() throws Exception { 111 | CachingStrategy cachingStrategy = new CachingStrategy() 112 | .withCacheName(MY_CACHE) 113 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 114 | RequestOptions requestOptions = new RequestOptions() 115 | .withCachingStrategy(cachingStrategy); 116 | 117 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 118 | GraphqlResponse response = graphqlClient.execute(dummy, Data.class, Error.class, requestOptions); 119 | assertEquals("Some text", response.getData().text); 120 | assertEquals(42, response.getData().count.intValue()); 121 | 122 | // This reponse is NOT coming from the cache, so we have to prepare the HTTP response again 123 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 124 | GraphqlResponse response2 = graphqlClient.execute(new GraphqlRequest("{dummy2}"), Data.class, Error.class, 125 | requestOptions); 126 | assertEquals("Some text", response2.getData().text); 127 | assertEquals(42, response2.getData().count.intValue()); 128 | 129 | // HTTP client was called twice 130 | Mockito.verify(graphqlClient.client, Mockito.times(2)).execute(Mockito.any(), Mockito.any(ResponseHandler.class)); 131 | } 132 | 133 | @Test 134 | public void testNoCacheDifferentHttpHeaders() throws Exception { 135 | CachingStrategy cachingStrategy = new CachingStrategy() 136 | .withCacheName(MY_CACHE) 137 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 138 | 139 | RequestOptions requestOptions1 = new RequestOptions() 140 | .withCachingStrategy(cachingStrategy) 141 | .withHeaders(Collections.singletonList(new BasicHeader("Some", "value1"))); 142 | 143 | RequestOptions requestOptions2 = new RequestOptions() 144 | .withCachingStrategy(cachingStrategy) 145 | .withHeaders(Collections.singletonList(new BasicHeader("Some", "value2"))); 146 | 147 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 148 | GraphqlResponse response = graphqlClient.execute(dummy, Data.class, Error.class, requestOptions1); 149 | assertEquals("Some text", response.getData().text); 150 | assertEquals(42, response.getData().count.intValue()); 151 | 152 | // This reponse is NOT coming from the cache, so we have to prepare the HTTP response again 153 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 154 | GraphqlResponse response2 = graphqlClient.execute(dummy, Data.class, Error.class, requestOptions2); 155 | assertEquals("Some text", response2.getData().text); 156 | assertEquals(42, response2.getData().count.intValue()); 157 | 158 | // HTTP client was called twice 159 | Mockito.verify(graphqlClient.client, Mockito.times(2)).execute(Mockito.any(), Mockito.any(ResponseHandler.class)); 160 | } 161 | 162 | @Test 163 | public void testDisabledCache() throws Exception { 164 | CachingStrategy cachingStrategy = new CachingStrategy() 165 | .withCacheName(MY_DISABLED_CACHE) 166 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 167 | RequestOptions requestOptions = new RequestOptions() 168 | .withCachingStrategy(cachingStrategy); 169 | 170 | assertNoCaching(dummy, requestOptions); 171 | } 172 | 173 | @Test 174 | public void testCachingDisabledByPolicy() throws Exception { 175 | CachingStrategy cachingStrategy = new CachingStrategy() 176 | .withCacheName(MY_CACHE) 177 | .withDataFetchingPolicy(DataFetchingPolicy.NETWORK_ONLY); 178 | RequestOptions requestOptions = new RequestOptions() 179 | .withCachingStrategy(cachingStrategy); 180 | 181 | assertNoCaching(dummy, requestOptions); 182 | } 183 | 184 | @Test 185 | public void testNoRequesOptions() throws Exception { 186 | assertNoCaching(dummy, null); 187 | } 188 | 189 | @Test 190 | public void testNoCachingStrategy() throws Exception { 191 | RequestOptions requestOptions = new RequestOptions(); 192 | assertNoCaching(dummy, requestOptions); 193 | } 194 | 195 | @Test 196 | public void testNoDataFetchingPolicy() throws Exception { 197 | CachingStrategy cachingStrategy = new CachingStrategy() 198 | .withCacheName(MY_CACHE); 199 | RequestOptions requestOptions = new RequestOptions() 200 | .withCachingStrategy(cachingStrategy); 201 | 202 | assertNoCaching(dummy, requestOptions); 203 | } 204 | 205 | @Test 206 | public void testNoCachingForMutation() throws Exception { 207 | CachingStrategy cachingStrategy = new CachingStrategy() 208 | .withCacheName(MY_CACHE) 209 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 210 | RequestOptions requestOptions = new RequestOptions() 211 | .withCachingStrategy(cachingStrategy); 212 | 213 | assertNoCaching(new GraphqlRequest("mutation{dummy}"), requestOptions); 214 | } 215 | 216 | @Test 217 | public void testNoCache() throws Exception { 218 | graphqlClient = new GraphqlClientImpl(); 219 | graphqlClient.activate(new MockGraphqlClientConfiguration(), mock(BundleContext.class)); 220 | graphqlClient.client = mock(HttpClient.class); 221 | 222 | CachingStrategy cachingStrategy = new CachingStrategy() 223 | .withCacheName(MY_CACHE) 224 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 225 | RequestOptions requestOptions = new RequestOptions() 226 | .withCachingStrategy(cachingStrategy); 227 | 228 | assertNoCaching(dummy, requestOptions); 229 | } 230 | 231 | @Test 232 | public void testEmptyCache() throws Exception { 233 | graphqlClient = new GraphqlClientImpl(); 234 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 235 | config.setCacheConfigurations(ArrayUtils.EMPTY_STRING_ARRAY); 236 | graphqlClient.activate(config, mock(BundleContext.class)); 237 | graphqlClient.client = mock(HttpClient.class); 238 | 239 | CachingStrategy cachingStrategy = new CachingStrategy() 240 | .withCacheName(MY_CACHE) 241 | .withDataFetchingPolicy(DataFetchingPolicy.CACHE_FIRST); 242 | RequestOptions requestOptions = new RequestOptions() 243 | .withCachingStrategy(cachingStrategy); 244 | 245 | assertNoCaching(dummy, requestOptions); 246 | } 247 | 248 | private void assertNoCaching(GraphqlRequest request, RequestOptions requestOptions) throws Exception { 249 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 250 | GraphqlResponse response = graphqlClient.execute(request, Data.class, Error.class, requestOptions); 251 | assertEquals("Some text", response.getData().text); 252 | assertEquals(42, response.getData().count.intValue()); 253 | 254 | // This reponse is NOT coming from the cache, so we have to prepare the HTTP response again 255 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 256 | GraphqlResponse response2 = graphqlClient.execute(request, Data.class, Error.class, requestOptions); 257 | assertEquals("Some text", response2.getData().text); 258 | assertEquals(42, response2.getData().count.intValue()); 259 | 260 | // HTTP client was called twice 261 | Mockito.verify(graphqlClient.client, Mockito.times(2)).execute(Mockito.any(), Mockito.any(ResponseHandler.class)); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientImplMetricsTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.io.IOException; 18 | import java.io.InputStream; 19 | 20 | import org.apache.http.HttpStatus; 21 | import org.apache.http.client.HttpClient; 22 | import org.apache.http.client.ResponseHandler; 23 | import org.apache.http.impl.client.HttpClientBuilder; 24 | import org.apache.http.osgi.services.HttpClientBuilderFactory; 25 | import org.apache.sling.testing.mock.osgi.MockOsgi; 26 | import org.junit.Before; 27 | import org.junit.Rule; 28 | import org.junit.Test; 29 | 30 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 31 | import com.codahale.metrics.Counter; 32 | import com.codahale.metrics.Gauge; 33 | import com.codahale.metrics.MetricRegistry; 34 | import com.codahale.metrics.Timer; 35 | import io.wcm.testing.mock.aem.junit.AemContext; 36 | 37 | import static org.junit.Assert.assertEquals; 38 | import static org.junit.Assert.assertNotNull; 39 | import static org.junit.Assert.fail; 40 | import static org.mockito.Matchers.any; 41 | import static org.mockito.Mockito.doThrow; 42 | import static org.mockito.Mockito.mock; 43 | 44 | public class GraphqlClientImplMetricsTest { 45 | 46 | @Rule 47 | public final AemContext aemContext = GraphqlAemContext.createContext(); 48 | private final MetricRegistry metricRegistry = new MetricRegistry(); 49 | private final GraphqlClientImpl graphqlClient = new GraphqlClientImpl(); 50 | private final GraphqlRequest dummy = new GraphqlRequest("{dummy-é}"); // with accent to check UTF-8 character 51 | 52 | private static class Data {} 53 | 54 | private static class Error {} 55 | 56 | @Before 57 | public void setUp() { 58 | aemContext.registerService(HttpClientBuilderFactory.class, HttpClientBuilder::create); 59 | aemContext.registerService(MetricRegistry.class, metricRegistry, "name", "cif"); 60 | aemContext.registerInjectActivateService(graphqlClient, 61 | "identifier", "default", 62 | "url", "https://foo.bar/api"); 63 | graphqlClient.client = mock(HttpClient.class); 64 | } 65 | 66 | @Test 67 | public void testRequestDurationTracked() throws IOException { 68 | // given 69 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 70 | 71 | // when 72 | graphqlClient.execute(dummy, Data.class, Error.class); 73 | 74 | // then 75 | Timer timer = metricRegistry.getTimers().get("graphql-client.request.duration;gql_client_endpoint=https://foo.bar/api"); 76 | assertNotNull(timer); 77 | assertEquals(1, timer.getCount()); 78 | } 79 | 80 | @Test 81 | public void testCacheMetricsAddedAndRemovedForMultipleCaches() throws IOException { 82 | // given 83 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 84 | MockOsgi.activate(graphqlClient, aemContext.bundleContext(), 85 | "identifier", "default", 86 | "url", "https://foo.bar/api", 87 | "cacheConfigurations", new String[] { 88 | "foo:true:100:100", 89 | "bar:true:100:100" 90 | }); 91 | 92 | // when, then 93 | Gauge fooHits = metricRegistry.getGauges().get("graphql-client.cache.hits;gql_client_identifier=default;gql_cache_name=foo"); 94 | assertNotNull(fooHits); 95 | Gauge fooMisses = metricRegistry.getGauges().get("graphql-client.cache.misses;gql_client_identifier=default;gql_cache_name=foo"); 96 | assertNotNull(fooMisses); 97 | Gauge fooEvictions = metricRegistry.getGauges().get( 98 | "graphql-client.cache.evictions;gql_client_identifier=default;gql_cache_name=foo"); 99 | assertNotNull(fooEvictions); 100 | Gauge fooUsage = metricRegistry.getGauges().get("graphql-client.cache.usage;gql_client_identifier=default;gql_cache_name=foo"); 101 | assertNotNull(fooUsage); 102 | Gauge barHits = metricRegistry.getGauges().get("graphql-client.cache.hits;gql_client_identifier=default;gql_cache_name=bar"); 103 | assertNotNull(barHits); 104 | Gauge barMisses = metricRegistry.getGauges().get("graphql-client.cache.misses;gql_client_identifier=default;gql_cache_name=bar"); 105 | assertNotNull(barMisses); 106 | Gauge barEvictions = metricRegistry.getGauges().get( 107 | "graphql-client.cache.evictions;gql_client_identifier=default;gql_cache_name=bar"); 108 | assertNotNull(barEvictions); 109 | Gauge barUsage = metricRegistry.getGauges().get("graphql-client.cache.usage;gql_client_identifier=default;gql_cache_name=bar"); 110 | assertNotNull(barUsage); 111 | 112 | // and when, then 113 | MockOsgi.deactivate(graphqlClient, aemContext.bundleContext()); 114 | assertEquals(0, metricRegistry.getGauges().size()); 115 | } 116 | 117 | @Test 118 | public void testConnectionPoolMetricsAddedAndRemoved() throws IOException { 119 | // given 120 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 121 | MockOsgi.activate(graphqlClient, aemContext.bundleContext(), 122 | "identifier", "default", "url", "https://example.com"); 123 | 124 | // when, then 125 | Gauge availableConnections = metricRegistry.getGauges().get( 126 | "graphql-client.connection-pool.available-connections;gql_client_identifier=default"); 127 | assertNotNull(availableConnections); 128 | Gauge pendingRequests = metricRegistry.getGauges().get( 129 | "graphql-client.connection-pool.pending-requests;gql_client_identifier=default"); 130 | assertNotNull(pendingRequests); 131 | Gauge usage = metricRegistry.getGauges().get("graphql-client.connection-pool.usage;gql_client_identifier=default"); 132 | assertNotNull(usage); 133 | 134 | // and when, then 135 | MockOsgi.deactivate(graphqlClient, aemContext.bundleContext()); 136 | assertEquals(0, metricRegistry.getGauges().size()); 137 | } 138 | 139 | @Test 140 | public void testRequestDurationNotTrackedOnClientError() throws IOException { 141 | // given 142 | doThrow(new IOException()).when(graphqlClient.client).execute(any(), any(ResponseHandler.class)); 143 | 144 | // when 145 | try { 146 | graphqlClient.execute(dummy, Data.class, Error.class); 147 | fail(); 148 | } catch (RuntimeException ex) { 149 | // expected 150 | } 151 | 152 | // then 153 | Timer timer = metricRegistry.getTimers().get("graphql-client.request.duration;gql_client_endpoint=https://foo.bar/api"); 154 | assertNotNull(timer); 155 | assertEquals(0, timer.getCount()); 156 | Counter counter = metricRegistry.getCounters().get("graphql-client.request.errors;gql_client_endpoint=https://foo.bar/api"); 157 | assertNotNull(counter); 158 | assertEquals(1, counter.getCount()); 159 | } 160 | 161 | @Test 162 | public void testRequestDurationNotTrackedOnEntityLoadError() throws IOException { 163 | // given 164 | InputStream is = new InputStream() { 165 | @Override 166 | public int read() throws IOException { 167 | throw new IOException(); 168 | } 169 | }; 170 | TestUtils.setupHttpResponse(is, graphqlClient.client, HttpStatus.SC_OK); 171 | 172 | // when 173 | try { 174 | graphqlClient.execute(dummy, Data.class, Error.class); 175 | fail(); 176 | } catch (RuntimeException ex) { 177 | // expected 178 | } 179 | 180 | // then 181 | Timer timer = metricRegistry.getTimers().get("graphql-client.request.duration;gql_client_endpoint=https://foo.bar/api"); 182 | assertNotNull(timer); 183 | assertEquals(0, timer.getCount()); 184 | Counter counter = metricRegistry.getCounters().get("graphql-client.request.errors;gql_client_endpoint=https://foo.bar/api"); 185 | assertNotNull(counter); 186 | assertEquals(1, counter.getCount()); 187 | } 188 | 189 | @Test 190 | public void testErrorCodeTrackedWithStatus() throws IOException { 191 | // given 192 | TestUtils.setupHttpResponse(mock(InputStream.class), graphqlClient.client, HttpStatus.SC_INTERNAL_SERVER_ERROR); 193 | 194 | // when 195 | try { 196 | graphqlClient.execute(dummy, Data.class, Error.class); 197 | fail(); 198 | } catch (RuntimeException ex) { 199 | // expected 200 | } 201 | 202 | // then 203 | Counter counter = metricRegistry.getCounters().get( 204 | "graphql-client.request.errors;gql_client_endpoint=https://foo.bar/api;gql_response_status=500"); 205 | assertNotNull(counter); 206 | assertEquals(1, counter.getCount()); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/GraphqlClientImplTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.io.IOException; 18 | import java.lang.reflect.Field; 19 | import java.lang.reflect.Type; 20 | import java.util.*; 21 | 22 | import org.apache.http.Header; 23 | import org.apache.http.HeaderIterator; 24 | import org.apache.http.HttpHeaders; 25 | import org.apache.http.HttpResponse; 26 | import org.apache.http.HttpStatus; 27 | import org.apache.http.client.HttpClient; 28 | import org.apache.http.client.ResponseHandler; 29 | import org.apache.http.client.methods.HttpUriRequest; 30 | import org.apache.http.conn.ConnectionKeepAliveStrategy; 31 | import org.apache.http.impl.client.HttpClientBuilder; 32 | import org.apache.http.message.BasicHeader; 33 | import org.apache.http.message.BasicListHeaderIterator; 34 | import org.apache.http.protocol.HTTP; 35 | import org.hamcrest.CustomMatcher; 36 | import org.junit.Before; 37 | import org.junit.Test; 38 | import org.mockito.ArgumentCaptor; 39 | import org.mockito.Mockito; 40 | import org.osgi.framework.BundleContext; 41 | import org.osgi.framework.Constants; 42 | import org.osgi.framework.ServiceRegistration; 43 | import org.slf4j.LoggerFactory; 44 | 45 | import ch.qos.logback.classic.Level; 46 | import ch.qos.logback.classic.Logger; 47 | import ch.qos.logback.classic.LoggerContext; 48 | import ch.qos.logback.classic.spi.ILoggingEvent; 49 | import ch.qos.logback.core.Appender; 50 | import com.adobe.cq.commerce.graphql.client.GraphqlClient; 51 | import com.adobe.cq.commerce.graphql.client.GraphqlClientConfiguration; 52 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 53 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 54 | import com.adobe.cq.commerce.graphql.client.HttpMethod; 55 | import com.adobe.cq.commerce.graphql.client.RequestOptions; 56 | import com.adobe.cq.commerce.graphql.client.impl.TestUtils.GetQueryMatcher; 57 | import com.adobe.cq.commerce.graphql.client.impl.TestUtils.HeadersMatcher; 58 | import com.adobe.cq.commerce.graphql.client.impl.TestUtils.RequestBodyMatcher; 59 | import com.google.gson.Gson; 60 | import com.google.gson.GsonBuilder; 61 | import com.google.gson.JsonDeserializationContext; 62 | import com.google.gson.JsonDeserializer; 63 | import com.google.gson.JsonElement; 64 | import com.google.gson.JsonParseException; 65 | 66 | import static org.junit.Assert.assertArrayEquals; 67 | import static org.junit.Assert.assertEquals; 68 | import static org.junit.Assert.assertNotNull; 69 | import static org.junit.Assert.assertNull; 70 | import static org.junit.Assert.assertTrue; 71 | import static org.mockito.Matchers.any; 72 | import static org.mockito.Matchers.anyString; 73 | import static org.mockito.Matchers.argThat; 74 | import static org.mockito.Matchers.eq; 75 | import static org.mockito.Mockito.mock; 76 | import static org.mockito.Mockito.never; 77 | import static org.mockito.Mockito.times; 78 | import static org.mockito.Mockito.verify; 79 | import static org.mockito.Mockito.when; 80 | 81 | public class GraphqlClientImplTest { 82 | 83 | private static final String AUTH_HEADER_VALUE = "Basic 1234"; 84 | private static final String CACHE_HEADER_VALUE = "max-age=300"; 85 | 86 | private static class Data { 87 | String text; 88 | Integer count; 89 | } 90 | 91 | private static class Error { 92 | String message; 93 | } 94 | 95 | private GraphqlClientImpl graphqlClient; 96 | private GraphqlRequest dummy = new GraphqlRequest("{dummy-é}"); // with accent to check UTF-8 character 97 | private MockGraphqlClientConfiguration mockConfig; 98 | private CacheInvalidator cacheInvalidator; 99 | private Field cacheInvalidatorField; 100 | 101 | @Before 102 | public void setUp() throws Exception { 103 | graphqlClient = new GraphqlClientImpl(); 104 | 105 | mockConfig = new MockGraphqlClientConfiguration(); 106 | mockConfig.setIdentifier("mockIdentifier"); 107 | // Add three test headers, one with extra white space around " : " to make sure we properly trim spaces, and one empty header 108 | mockConfig.setHttpHeaders( 109 | HttpHeaders.AUTHORIZATION + ":" + AUTH_HEADER_VALUE, 110 | HttpHeaders.CACHE_CONTROL + " : " + CACHE_HEADER_VALUE); 111 | 112 | graphqlClient.activate(mockConfig, mock(BundleContext.class)); 113 | graphqlClient.client = Mockito.mock(HttpClient.class); 114 | // Use reflection to set the private cacheInvalidator field 115 | cacheInvalidator = mock(CacheInvalidator.class); 116 | cacheInvalidatorField = GraphqlClientImpl.class.getDeclaredField("cacheInvalidator"); 117 | cacheInvalidatorField.setAccessible(true); 118 | } 119 | 120 | @Test 121 | public void testRegistersAsGraphqlClientService() throws Exception { 122 | ArgumentCaptor serviceProps = ArgumentCaptor.forClass(Dictionary.class); 123 | // given 124 | BundleContext bundleContext = mock(BundleContext.class); 125 | ServiceRegistration registration = mock(ServiceRegistration.class); 126 | when(bundleContext.registerService(eq(GraphqlClient.class), eq(graphqlClient), serviceProps.capture())).thenReturn(registration); 127 | mockConfig.setServiceRanking(200); 128 | 129 | // when 130 | graphqlClient.activate(mockConfig, bundleContext); 131 | 132 | // then 133 | verify(bundleContext).registerService(eq(GraphqlClient.class), eq(graphqlClient), any()); 134 | assertEquals(200, serviceProps.getValue().get(Constants.SERVICE_RANKING)); 135 | assertEquals("mockIdentifier", serviceProps.getValue().get("identifier")); 136 | 137 | // and when 138 | graphqlClient.deactivate(); 139 | 140 | // then 141 | verify(registration).unregister(); 142 | } 143 | 144 | @Test 145 | public void testEmptyUrlRegistersNoService() throws Exception { 146 | BundleContext bundleContext = mock(BundleContext.class); 147 | mockConfig.setUrl(""); 148 | graphqlClient.activate(mockConfig, bundleContext); 149 | verify(bundleContext, never()).registerService(any(Class.class), any(GraphqlClientImpl.class), any()); 150 | // verify that no exception is thrown 151 | graphqlClient.deactivate(); 152 | } 153 | 154 | @Test 155 | public void testInvalidUrlRegistersNoService() throws Exception { 156 | BundleContext bundleContext = mock(BundleContext.class); 157 | mockConfig.setUrl("$[env:URL]"); 158 | graphqlClient.activate(mockConfig, bundleContext); 159 | verify(bundleContext, never()).registerService(any(Class.class), any(GraphqlClientImpl.class), any()); 160 | // verify that no exception is thrown 161 | graphqlClient.deactivate(); 162 | } 163 | 164 | @Test 165 | public void testInvalidTimeouts() throws Exception { 166 | mockConfig.setSocketTimeout(0); 167 | mockConfig.setConnectionTimeout(0); 168 | mockConfig.setRequestPoolTimeout(0); 169 | graphqlClient.activate(mockConfig, mock(BundleContext.class)); 170 | 171 | assertEquals(GraphqlClientConfiguration.DEFAULT_SOCKET_TIMEOUT, graphqlClient.getConfiguration().socketTimeout()); 172 | assertEquals(GraphqlClientConfiguration.DEFAULT_CONNECTION_TIMEOUT, graphqlClient.getConfiguration().connectionTimeout()); 173 | assertEquals(GraphqlClientConfiguration.DEFAULT_REQUESTPOOL_TIMEOUT, graphqlClient.getConfiguration().requestPoolTimeout()); 174 | } 175 | 176 | @Test 177 | public void testWarningsAreLoggedForInvalidConfigurations() throws Exception { 178 | LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory(); 179 | Logger logger = loggerContext.getLogger(GraphqlClientImpl.class); 180 | logger.setLevel(Level.WARN); 181 | Appender appender = mock(Appender.class, "testWarningsAreLoggedForTimeoutsTooBig"); 182 | try { 183 | logger.addAppender(appender); 184 | 185 | mockConfig.setSocketTimeout(10000); 186 | mockConfig.setConnectionTimeout(10000); 187 | mockConfig.setRequestPoolTimeout(10000); 188 | mockConfig.setHttpHeaders(""); 189 | graphqlClient.activate(mockConfig, mock(BundleContext.class)); 190 | 191 | // verify the 3 warnings are logged 192 | verify(appender, times(4)).doAppend(argThat(new CustomMatcher("log event of level warn") { 193 | @Override 194 | public boolean matches(Object o) { 195 | return o instanceof ILoggingEvent && ((ILoggingEvent) o).getLevel() == Level.WARN; 196 | } 197 | })); 198 | } finally { 199 | logger.detachAppender(appender); 200 | } 201 | } 202 | 203 | @Test 204 | public void testInvalidHttpHeaders() throws Exception { 205 | mockConfig.setHttpHeaders("anything", "", ":Value", "Name: ", "Header: Value"); 206 | graphqlClient.activate(mockConfig, mock(BundleContext.class)); 207 | assertArrayEquals(new String[] { "Header: Value" }, graphqlClient.getConfiguration().httpHeaders()); 208 | } 209 | 210 | @Test 211 | public void testInvalidHttpHeadersSkipped() throws Exception { 212 | // should not be possible in real world, but may happen if a regression is introduced that exposes the setHttpHeaders() of the 213 | // configuration 214 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 215 | GraphqlClientConfigurationImpl activeConfig = (GraphqlClientConfigurationImpl) graphqlClient.getConfiguration(); 216 | activeConfig.setHttpHeaders("anything", "", ":Value", "Name: ", "Header: Value"); 217 | graphqlClient.execute(dummy, Data.class, Error.class); 218 | 219 | List
expectedHeaders = new ArrayList<>(); 220 | expectedHeaders.add(new BasicHeader("Header", "Value")); 221 | 222 | // Check that the HTTP client is sending the custom request headers and the headers set in the OSGi config 223 | HeadersMatcher matcher = new HeadersMatcher(expectedHeaders); 224 | Mockito.verify(graphqlClient.client, Mockito.times(1)).execute(Mockito.argThat(matcher), Mockito.any(ResponseHandler.class)); 225 | } 226 | 227 | @Test 228 | public void testRequestResponse() throws Exception { 229 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 230 | 231 | dummy.setOperationName("customOperation"); 232 | dummy.setVariables(Collections.singletonMap("variableName", "variableValue")); 233 | GraphqlResponse response = graphqlClient.execute(dummy, Data.class, Error.class); 234 | 235 | // Check that the query is what we expect 236 | String body = TestUtils.getResource("sample-graphql-request.json"); 237 | RequestBodyMatcher matcher = new RequestBodyMatcher(body); 238 | Mockito.verify(graphqlClient.client, Mockito.times(1)).execute(Mockito.argThat(matcher), Mockito.any(ResponseHandler.class)); 239 | 240 | // Check the response data 241 | assertEquals("Some text", response.getData().text); 242 | assertEquals(42, response.getData().count.intValue()); 243 | 244 | // Check the response errors 245 | assertEquals(1, response.getErrors().size()); 246 | Error error = response.getErrors().get(0); 247 | assertEquals("Error message", error.message); 248 | } 249 | 250 | @Test 251 | public void testHttpError() throws Exception { 252 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_SERVICE_UNAVAILABLE); 253 | Exception exception = null; 254 | try { 255 | graphqlClient.execute(dummy, Data.class, Error.class); 256 | } catch (Exception e) { 257 | exception = e; 258 | } 259 | assertEquals("GraphQL query failed with response code 503", exception.getMessage()); 260 | } 261 | 262 | @Test 263 | @SuppressWarnings("unchecked") 264 | public void testHttpClientException() throws Exception { 265 | when(graphqlClient.client.execute(any(HttpUriRequest.class), any(ResponseHandler.class))).thenThrow(IOException.class); 266 | Exception exception = null; 267 | try { 268 | graphqlClient.execute(dummy, Data.class, Error.class); 269 | } catch (Exception e) { 270 | exception = e; 271 | } 272 | assertEquals("Failed to send GraphQL request", exception.getMessage()); 273 | } 274 | 275 | @Test 276 | public void testInvalidResponse() throws Exception { 277 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 278 | Exception exception = null; 279 | try { 280 | graphqlClient.execute(dummy, String.class, String.class); 281 | } catch (Exception e) { 282 | exception = e; 283 | } 284 | assertNotNull(exception); 285 | } 286 | 287 | @Test 288 | public void testHttpResponseError() throws Exception { 289 | TestUtils.setupNullResponse(graphqlClient.client); 290 | Exception exception = null; 291 | try { 292 | graphqlClient.execute(dummy, String.class, String.class); 293 | } catch (Exception e) { 294 | exception = e; 295 | } 296 | assertEquals("Failed to read HTTP response content", exception.getMessage()); 297 | } 298 | 299 | @Test 300 | public void testCustomGson() throws Exception { 301 | // A custom deserializer that returns dummy data 302 | class CustomDeserializer implements JsonDeserializer { 303 | public Data deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { 304 | Data data = new Data(); 305 | data.text = "customText"; 306 | data.count = 4242; 307 | return data; 308 | } 309 | } 310 | 311 | Gson gson = new GsonBuilder().registerTypeAdapter(Data.class, new CustomDeserializer()).create(); 312 | 313 | // The response from the JSON data is overwritten by the custom deserializer 314 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 315 | 316 | GraphqlResponse response = graphqlClient.execute(dummy, Data.class, Error.class, new RequestOptions().withGson(gson)); 317 | assertEquals("customText", response.getData().text); 318 | assertEquals(4242, response.getData().count.intValue()); 319 | } 320 | 321 | @Test 322 | public void testHeaders() throws Exception { 323 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 324 | List
requestHeaders = Collections.singletonList(new BasicHeader("customName", "customValue")); 325 | graphqlClient.execute(dummy, Data.class, Error.class, new RequestOptions().withHeaders(requestHeaders)); 326 | 327 | List
expectedHeaders = new ArrayList<>(); 328 | expectedHeaders.addAll(requestHeaders); 329 | expectedHeaders.add(new BasicHeader(HttpHeaders.AUTHORIZATION, AUTH_HEADER_VALUE)); 330 | expectedHeaders.add(new BasicHeader(HttpHeaders.CACHE_CONTROL, CACHE_HEADER_VALUE)); 331 | 332 | // Check that the HTTP client is sending the custom request headers and the headers set in the OSGi config 333 | HeadersMatcher matcher = new HeadersMatcher(expectedHeaders); 334 | Mockito.verify(graphqlClient.client, Mockito.times(1)).execute(Mockito.argThat(matcher), Mockito.any(ResponseHandler.class)); 335 | } 336 | 337 | @Test 338 | public void testGetHttpMethod() throws Exception { 339 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 340 | graphqlClient.execute(dummy, Data.class, Error.class, new RequestOptions().withHttpMethod(HttpMethod.GET)); 341 | 342 | // Check that the GraphQL request is properly encoded in the URL 343 | GetQueryMatcher matcher = new GetQueryMatcher(dummy); 344 | Mockito.verify(graphqlClient.client, Mockito.times(1)).execute(Mockito.argThat(matcher), Mockito.any(ResponseHandler.class)); 345 | } 346 | 347 | @Test 348 | public void testGetHttpMethodWithVariables() throws Exception { 349 | String query = "query MyQuery($arg: String) {something(arg: $arg) {field}}"; 350 | GraphqlRequest request = new GraphqlRequest(query); 351 | request.setOperationName("MyQuery"); 352 | request.setVariables(Collections.singletonMap("arg", "something")); 353 | 354 | TestUtils.setupHttpResponse("sample-graphql-response.json", graphqlClient.client, HttpStatus.SC_OK); 355 | graphqlClient.execute(request, Data.class, Error.class, new RequestOptions().withHttpMethod(HttpMethod.GET)); 356 | 357 | // Check that the GraphQL request is properly encoded in the URL 358 | GetQueryMatcher matcher = new GetQueryMatcher(request); 359 | Mockito.verify(graphqlClient.client, Mockito.times(1)).execute(Mockito.argThat(matcher), Mockito.any(ResponseHandler.class)); 360 | } 361 | 362 | @Test 363 | public void testGetGraphQLEndpoint() { 364 | String endpointURL = graphqlClient.getGraphQLEndpoint(); 365 | assertEquals(MockGraphqlClientConfiguration.URL, endpointURL); 366 | } 367 | 368 | @Test 369 | public void testGetConfiguration() { 370 | GraphqlClientConfiguration configuration = graphqlClient.getConfiguration(); 371 | assertEquals(mockConfig.identifier(), configuration.identifier()); 372 | assertEquals("mockIdentifier", graphqlClient.getIdentifier()); 373 | } 374 | 375 | @Test 376 | public void testDefaultConnectionKeepAlive() throws Exception { 377 | graphqlClient = new GraphqlClientImpl(); 378 | mockConfig = new MockGraphqlClientConfiguration(); 379 | graphqlClient.activate(mockConfig, mock(BundleContext.class)); 380 | HttpClientBuilder builder = graphqlClient.configureHttpClientBuilder(); 381 | assertNull(getBuilderKeepAliveStrategy(builder)); 382 | } 383 | 384 | @Test 385 | public void testCustomConnectionKeepAlive() throws Exception { 386 | int customKeepAlive = 10; 387 | 388 | graphqlClient = new GraphqlClientImpl(); 389 | mockConfig = new MockGraphqlClientConfiguration(); 390 | mockConfig.setConnectionKeepAlive(customKeepAlive); 391 | graphqlClient.activate(mockConfig, mock(BundleContext.class)); 392 | HttpClientBuilder builder = graphqlClient.configureHttpClientBuilder(); 393 | ConnectionKeepAliveStrategy connectionKeepAliveStrategy = getBuilderKeepAliveStrategy(builder); 394 | 395 | assertTrue(connectionKeepAliveStrategy instanceof GraphqlClientImpl.ConfigurableConnectionKeepAliveStrategy); 396 | 397 | // with empty headers 398 | HttpResponse httpResponse = mock(HttpResponse.class); 399 | when(httpResponse.headerIterator(anyString())).thenReturn(mock(HeaderIterator.class)); 400 | assertEquals(customKeepAlive * 1000L, connectionKeepAliveStrategy.getKeepAliveDuration(httpResponse, null)); 401 | 402 | // with keep alive header timeout invalid 403 | prepareResponse(httpResponse, "2.5"); 404 | assertEquals(customKeepAlive * 1000L, connectionKeepAliveStrategy.getKeepAliveDuration(httpResponse, null)); 405 | 406 | // with keep alive header timeout negative 407 | prepareResponse(httpResponse, "-1"); 408 | assertEquals(customKeepAlive * 1000L, connectionKeepAliveStrategy.getKeepAliveDuration(httpResponse, null)); 409 | 410 | // with keep alive header timeout smaller than custom 411 | int responseKeepAlive = 5; 412 | prepareResponse(httpResponse, String.valueOf(responseKeepAlive)); 413 | assertEquals(responseKeepAlive * 1000L, connectionKeepAliveStrategy.getKeepAliveDuration(httpResponse, null)); 414 | 415 | // with keep alive header timeout larger than custom 416 | prepareResponse(httpResponse, "15"); 417 | assertEquals(customKeepAlive * 1000L, connectionKeepAliveStrategy.getKeepAliveDuration(httpResponse, null)); 418 | } 419 | 420 | @Test 421 | public void testInvalidateCache() throws NoSuchFieldException, IllegalAccessException { 422 | String storeView = "default"; 423 | String[] cacheNames = { "cache1", "cache2" }; 424 | String[] patterns = { "pattern1", "pattern2" }; 425 | 426 | cacheInvalidatorField.set(graphqlClient, cacheInvalidator); 427 | 428 | graphqlClient.invalidateCache(storeView, cacheNames, patterns); 429 | verify(cacheInvalidator).invalidateCache(storeView, cacheNames, patterns); 430 | } 431 | 432 | @Test 433 | public void testInvalidateCacheWhenCacheInvalidatorAsNull() throws NoSuchFieldException, IllegalAccessException { 434 | cacheInvalidatorField.set(graphqlClient, null); 435 | graphqlClient.invalidateCache(null, null, null); 436 | verify(cacheInvalidator, never()).invalidateCache(anyString(), any(), any()); 437 | } 438 | 439 | private void prepareResponse(HttpResponse httpResponse, String responseKeepAlive) { 440 | Header header = mock(Header.class); 441 | when(header.getName()).thenReturn(HTTP.CONN_KEEP_ALIVE); 442 | when(header.getValue()).thenReturn("timeout=" + responseKeepAlive); 443 | when(httpResponse.headerIterator(anyString())).thenReturn(new BasicListHeaderIterator(Collections.singletonList(header), null)); 444 | } 445 | 446 | ConnectionKeepAliveStrategy getBuilderKeepAliveStrategy(HttpClientBuilder builder) throws Exception { 447 | Field field = builder.getClass().getDeclaredField("keepAliveStrategy"); 448 | field.setAccessible(true); 449 | return (ConnectionKeepAliveStrategy) field.get(builder); 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/MockGraphqlClientConfiguration.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | package com.adobe.cq.commerce.graphql.client.impl; 15 | 16 | class MockGraphqlClientConfiguration extends GraphqlClientConfigurationImpl { 17 | 18 | public static final String URL = "https://hostname/graphql"; 19 | 20 | public MockGraphqlClientConfiguration() { 21 | super(URL); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/MockServerHelper.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.io.IOException; 18 | import java.nio.charset.StandardCharsets; 19 | import java.util.concurrent.TimeUnit; 20 | 21 | import org.apache.commons.io.IOUtils; 22 | import org.apache.http.HttpHeaders; 23 | import org.mockserver.client.MockServerClient; 24 | import org.mockserver.configuration.ConfigurationProperties; 25 | import org.mockserver.integration.ClientAndServer; 26 | import org.mockserver.model.HttpRequest; 27 | import org.mockserver.model.HttpResponse; 28 | import org.slf4j.Logger; 29 | import org.slf4j.LoggerFactory; 30 | 31 | import com.adobe.cq.commerce.graphql.client.GraphqlClient; 32 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 33 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 34 | 35 | import static org.junit.Assert.assertEquals; 36 | 37 | public class MockServerHelper { 38 | 39 | private static final Logger LOGGER = LoggerFactory.getLogger(MockServerHelper.class); 40 | 41 | static { 42 | ConfigurationProperties.logLevel("ERROR"); 43 | } 44 | 45 | private final ClientAndServer mockServer; 46 | private final GraphqlRequest dummy = new GraphqlRequest("{dummy}"); 47 | 48 | public MockServerHelper() { 49 | mockServer = ClientAndServer.startClientAndServer(); 50 | LOGGER.info("Started HTTP mock server on port {}", mockServer.getLocalPort()); 51 | } 52 | 53 | public GraphqlResponse executeGraphqlClientDummyRequest(GraphqlClient graphqlClient) 54 | throws IOException { 55 | return graphqlClient.execute(dummy, Data.class, Error.class); 56 | } 57 | 58 | public MockServerClient resetWithSampleResponse() { 59 | String json = getResource("sample-graphql-response.json"); 60 | MockServerClient mockServerClient = mockServer.reset(); 61 | mockServerClient 62 | .when(HttpRequest.request().withPath("/graphql")) 63 | .respond(HttpResponse.response().withHeader(HttpHeaders.CONTENT_TYPE, "application/json") 64 | .withHeader(HttpHeaders.CONNECTION, "keep-alive").withBody(json) 65 | .withDelay(TimeUnit.MILLISECONDS, 50)); 66 | 67 | return mockServerClient; 68 | } 69 | 70 | public void validateSampleResponse(GraphqlResponse response) throws IOException { 71 | assertEquals("Some text", response.getData().text); 72 | assertEquals(42, response.getData().count.intValue()); 73 | 74 | // Check the response errors 75 | assertEquals(1, response.getErrors().size()); 76 | Error error = response.getErrors().get(0); 77 | assertEquals("Error message", error.message); 78 | } 79 | 80 | private static String getResource(String filename) { 81 | try { 82 | return IOUtils.toString(MockServerHelper.class.getClassLoader().getResourceAsStream(filename), 83 | StandardCharsets.UTF_8); 84 | } catch (IOException ex) { 85 | throw new RuntimeException(ex); 86 | } 87 | } 88 | 89 | public MockServerClient reset() { 90 | return mockServer.reset(); 91 | } 92 | 93 | public void stop() { 94 | mockServer.stop(); 95 | } 96 | 97 | public Integer getLocalPort() { 98 | return mockServer.getLocalPort(); 99 | } 100 | 101 | public static class Data { 102 | String text; 103 | Integer count; 104 | } 105 | 106 | public static class Error { 107 | String message; 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/ProtocolTest.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2020 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import org.junit.AfterClass; 18 | import org.junit.BeforeClass; 19 | import org.junit.Test; 20 | import org.mockserver.client.MockServerClient; 21 | import org.mockserver.model.HttpRequest; 22 | import org.osgi.framework.BundleContext; 23 | 24 | import com.adobe.cq.commerce.graphql.client.GraphqlResponse; 25 | import com.adobe.cq.commerce.graphql.client.impl.MockServerHelper.Data; 26 | import com.adobe.cq.commerce.graphql.client.impl.MockServerHelper.Error; 27 | 28 | import static org.mockito.Matchers.any; 29 | import static org.mockito.Mockito.mock; 30 | import static org.mockito.Mockito.never; 31 | import static org.mockito.Mockito.verify; 32 | 33 | public class ProtocolTest { 34 | 35 | private static MockServerHelper mockServer; 36 | private static String JAVA_VERSION = System.getProperty("java.version"); 37 | 38 | @BeforeClass 39 | public static void initServer() { 40 | mockServer = new MockServerHelper(); 41 | mockServer.resetWithSampleResponse(); 42 | } 43 | 44 | @AfterClass 45 | public static void stopServer() { 46 | mockServer.stop(); 47 | } 48 | 49 | @Test(timeout = 15000) 50 | public void testSimpleRequest_HTTPS() throws Exception { 51 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 52 | config.setUrl("https://localhost:" + mockServer.getLocalPort() + "/graphql"); 53 | config.setAcceptSelfSignedCertificates(true); 54 | 55 | GraphqlClientImpl graphqlClient = new GraphqlClientImpl(); 56 | graphqlClient.activate(config, mock(BundleContext.class)); 57 | 58 | GraphqlResponse response = mockServer.executeGraphqlClientDummyRequest(graphqlClient); 59 | mockServer.validateSampleResponse(response); 60 | } 61 | 62 | /** 63 | * Ensure HTTP communication is by default not allowed. 64 | */ 65 | @Test 66 | public void testSimpleRequest_HTTP_Disallowed() throws Exception { 67 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 68 | config.setUrl("http://localhost:" + mockServer.getLocalPort() + "/graphql"); 69 | 70 | BundleContext bundleContext = mock(BundleContext.class); 71 | GraphqlClientImpl graphqlClient = new GraphqlClientImpl(); 72 | graphqlClient.activate(config, bundleContext); 73 | verify(bundleContext, never()).registerService(any(Class.class), any(GraphqlClientImpl.class), any()); 74 | } 75 | 76 | /** 77 | * HTTP communication should work if enabled via configuration. 78 | */ 79 | @Test(timeout = 15000) 80 | public void testSimpleRequest_HTTP_Allowed() throws Exception { 81 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 82 | config.setUrl("http://localhost:" + mockServer.getLocalPort() + "/graphql"); 83 | config.setAllowHttpProtocol(true); 84 | 85 | GraphqlClientImpl graphqlClient = new GraphqlClientImpl(); 86 | graphqlClient.activate(config, mock(BundleContext.class)); 87 | 88 | GraphqlResponse response = mockServer.executeGraphqlClientDummyRequest(graphqlClient); 89 | mockServer.validateSampleResponse(response); 90 | } 91 | 92 | @Test(timeout = 15000) 93 | public void testUserAgent() throws Exception { 94 | MockGraphqlClientConfiguration config = new MockGraphqlClientConfiguration(); 95 | config.setUrl("https://localhost:" + mockServer.getLocalPort() + "/graphql"); 96 | config.setAcceptSelfSignedCertificates(true); 97 | 98 | GraphqlClientImpl graphqlClient = new GraphqlClientImpl(); 99 | graphqlClient.activate(config, mock(BundleContext.class)); 100 | 101 | MockServerClient client = mockServer.resetWithSampleResponse(); 102 | mockServer.executeGraphqlClientDummyRequest(graphqlClient); 103 | 104 | client.verify(HttpRequest.request().withHeader("user-agent", "Adobe-CifGraphqlClient/TEST (Java/" + JAVA_VERSION + ")")); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/test/java/com/adobe/cq/commerce/graphql/client/impl/TestUtils.java: -------------------------------------------------------------------------------- 1 | /******************************************************************************* 2 | * 3 | * Copyright 2019 Adobe. All rights reserved. 4 | * This file is licensed to you under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. You may obtain a copy 6 | * of the License at http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software distributed under 9 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 10 | * OF ANY KIND, either express or implied. See the License for the specific language 11 | * governing permissions and limitations under the License. 12 | * 13 | ******************************************************************************/ 14 | 15 | package com.adobe.cq.commerce.graphql.client.impl; 16 | 17 | import java.io.ByteArrayInputStream; 18 | import java.io.IOException; 19 | import java.io.InputStream; 20 | import java.io.UnsupportedEncodingException; 21 | import java.net.URLEncoder; 22 | import java.nio.charset.StandardCharsets; 23 | import java.util.List; 24 | 25 | import org.apache.commons.io.IOUtils; 26 | import org.apache.http.Header; 27 | import org.apache.http.HttpEntity; 28 | import org.apache.http.HttpEntityEnclosingRequest; 29 | import org.apache.http.HttpResponse; 30 | import org.apache.http.HttpStatus; 31 | import org.apache.http.StatusLine; 32 | import org.apache.http.client.HttpClient; 33 | import org.apache.http.client.ResponseHandler; 34 | import org.apache.http.client.methods.HttpUriRequest; 35 | import org.mockito.ArgumentMatcher; 36 | import org.mockito.Mockito; 37 | 38 | import com.adobe.cq.commerce.graphql.client.GraphqlRequest; 39 | import com.adobe.cq.commerce.graphql.client.HttpMethod; 40 | import com.google.gson.Gson; 41 | 42 | public class TestUtils { 43 | 44 | /** 45 | * Matcher class used to check that the GraphQL request body is properly set. 46 | */ 47 | public static class RequestBodyMatcher extends ArgumentMatcher { 48 | 49 | private String body; 50 | 51 | public RequestBodyMatcher(String body) { 52 | this.body = body; 53 | } 54 | 55 | @Override 56 | public boolean matches(Object obj) { 57 | if (!(obj instanceof HttpUriRequest) && !(obj instanceof HttpEntityEnclosingRequest)) { 58 | return false; 59 | } 60 | HttpEntityEnclosingRequest req = (HttpEntityEnclosingRequest) obj; 61 | try { 62 | String body = IOUtils.toString(req.getEntity().getContent(), StandardCharsets.UTF_8); 63 | return body.equals(this.body); 64 | } catch (Exception e) { 65 | return false; 66 | } 67 | } 68 | 69 | } 70 | 71 | /** 72 | * Matcher class used to check that the headers are properly passed to the HTTP client. 73 | */ 74 | public static class HeadersMatcher extends ArgumentMatcher { 75 | 76 | private List
headers; 77 | 78 | public HeadersMatcher(List
headers) { 79 | this.headers = headers; 80 | } 81 | 82 | @Override 83 | public boolean matches(Object obj) { 84 | if (!(obj instanceof HttpUriRequest)) { 85 | return false; 86 | } 87 | HttpUriRequest req = (HttpUriRequest) obj; 88 | for (Header header : headers) { 89 | Header reqHeader = req.getFirstHeader(header.getName()); 90 | if (reqHeader == null || !reqHeader.getValue().equals(header.getValue())) { 91 | return false; 92 | } 93 | } 94 | return true; 95 | } 96 | } 97 | 98 | public static String encode(String str) throws UnsupportedEncodingException { 99 | return URLEncoder.encode(str, StandardCharsets.UTF_8.name()); 100 | } 101 | 102 | /** 103 | * Matcher class used to check that the GraphQL query is properly set and encoded when sent with a GET request. 104 | */ 105 | public static class GetQueryMatcher extends ArgumentMatcher { 106 | 107 | GraphqlRequest request; 108 | 109 | public GetQueryMatcher(GraphqlRequest request) { 110 | this.request = request; 111 | } 112 | 113 | @Override 114 | public boolean matches(Object obj) { 115 | if (!(obj instanceof HttpUriRequest)) { 116 | return false; 117 | } 118 | HttpUriRequest req = (HttpUriRequest) obj; 119 | String expectedEncodedQuery = MockGraphqlClientConfiguration.URL; 120 | try { 121 | expectedEncodedQuery += "?query=" + encode(request.getQuery()); 122 | if (request.getOperationName() != null) { 123 | expectedEncodedQuery += "&operationName=" + encode(request.getOperationName()); 124 | } 125 | if (request.getVariables() != null) { 126 | String json = new Gson().toJson(request.getVariables()); 127 | expectedEncodedQuery += "&variables=" + encode(json); 128 | } 129 | } catch (UnsupportedEncodingException e) { 130 | return false; 131 | } 132 | return HttpMethod.GET.toString().equals(req.getMethod()) && expectedEncodedQuery.equals(req.getURI().toString()); 133 | } 134 | } 135 | 136 | /** 137 | * This method prepares the mock http response with either the content of the filename 138 | * or the provided content String.
139 | *
140 | * Important: because of the way the content of an HTTP response is consumed, this method MUST be called each time 141 | * the client is called. 142 | * 143 | * @param filename The file to use for the json response. 144 | * @param httpClient The HTTP client for which we want to mock responses. 145 | * @param httpCode The http code that the mocked response will return. 146 | * 147 | * @return The JSON content of that file. 148 | * 149 | * @throws IOException 150 | */ 151 | public static String setupHttpResponse(String filename, HttpClient httpClient, int httpCode) throws IOException { 152 | String json = getResource(filename); 153 | 154 | HttpEntity mockedHttpEntity = Mockito.mock(HttpEntity.class); 155 | HttpResponse mockedHttpResponse = Mockito.mock(HttpResponse.class); 156 | StatusLine mockedStatusLine = Mockito.mock(StatusLine.class); 157 | 158 | byte[] bytes = json.getBytes(StandardCharsets.UTF_8); 159 | Mockito.when(mockedHttpEntity.getContent()).thenReturn(new ByteArrayInputStream(bytes)); 160 | Mockito.when(mockedHttpEntity.getContentLength()).thenReturn(new Long(bytes.length)); 161 | 162 | Mockito.when(mockedHttpResponse.getEntity()).thenReturn(mockedHttpEntity); 163 | Mockito.doAnswer(inv -> { 164 | ResponseHandler responseHandler = inv.getArgumentAt(1, ResponseHandler.class); 165 | return responseHandler.handleResponse(mockedHttpResponse); 166 | }).when(httpClient).execute(Mockito.any(HttpUriRequest.class), Mockito.any(ResponseHandler.class)); 167 | 168 | Mockito.when(mockedStatusLine.getStatusCode()).thenReturn(httpCode); 169 | Mockito.when(mockedHttpResponse.getStatusLine()).thenReturn(mockedStatusLine); 170 | 171 | return json; 172 | } 173 | 174 | public static void setupHttpResponse(InputStream data, HttpClient httpClient, int httpCode) throws IOException { 175 | HttpEntity mockedHttpEntity = Mockito.mock(HttpEntity.class); 176 | HttpResponse mockedHttpResponse = Mockito.mock(HttpResponse.class); 177 | StatusLine mockedStatusLine = Mockito.mock(StatusLine.class); 178 | 179 | Mockito.when(mockedHttpEntity.getContent()).thenReturn(data); 180 | 181 | Mockito.when(mockedHttpResponse.getEntity()).thenReturn(mockedHttpEntity); 182 | Mockito.doAnswer(inv -> { 183 | ResponseHandler responseHandler = inv.getArgumentAt(1, ResponseHandler.class); 184 | return responseHandler.handleResponse(mockedHttpResponse); 185 | }).when(httpClient).execute(Mockito.any(HttpUriRequest.class), Mockito.any(ResponseHandler.class)); 186 | 187 | Mockito.when(mockedStatusLine.getStatusCode()).thenReturn(httpCode); 188 | Mockito.when(mockedHttpResponse.getStatusLine()).thenReturn(mockedStatusLine); 189 | } 190 | 191 | public static void setupNullResponse(HttpClient httpClient) throws IOException { 192 | HttpResponse mockedHttpResponse = Mockito.mock(HttpResponse.class); 193 | StatusLine mockedStatusLine = Mockito.mock(StatusLine.class); 194 | 195 | Mockito.when(mockedHttpResponse.getEntity()).thenReturn(null); 196 | Mockito.doAnswer(inv -> { 197 | ResponseHandler responseHandler = inv.getArgumentAt(1, ResponseHandler.class); 198 | return responseHandler.handleResponse(mockedHttpResponse); 199 | }).when(httpClient).execute(Mockito.any(HttpUriRequest.class), Mockito.any(ResponseHandler.class)); 200 | 201 | Mockito.when(mockedStatusLine.getStatusCode()).thenReturn(HttpStatus.SC_OK); 202 | Mockito.when(mockedHttpResponse.getStatusLine()).thenReturn(mockedStatusLine); 203 | } 204 | 205 | public static String getResource(String filename) throws IOException { 206 | return IOUtils.toString(GraphqlClientImplTest.class.getClassLoader().getResourceAsStream(filename), StandardCharsets.UTF_8); 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /src/test/resources/com/adobe/cq/commerce/graphql/client/version.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2021 Adobe. All rights reserved. 3 | # This file is licensed to you under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. You may obtain a copy 5 | # of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | # 7 | # Unless required by applicable law or agreed to in writing, software distributed under 8 | # the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS 9 | # OF ANY KIND, either express or implied. See the License for the specific language 10 | # governing permissions and limitations under the License. 11 | # 12 | info.module = CifGraphqlClient 13 | info.release = TEST -------------------------------------------------------------------------------- /src/test/resources/context/graphql-client-adapter-factory-context.json: -------------------------------------------------------------------------------- 1 | { 2 | "pageA": { 3 | "jcr:primaryType": "cq:Page", 4 | "jcr:content": { 5 | "jcr:primaryType":"cq:PageContent", 6 | "cq:graphqlClient": "my-catalog" 7 | } 8 | }, 9 | "pageB": { 10 | "jcr:primaryType": "cq:Page", 11 | "jcr:content": { 12 | "jcr:primaryType":"cq:PageContent", 13 | "cq:graphqlClient": "my-catalog" 14 | }, 15 | "pageC": { 16 | "jcr:primaryType": "cq:Page", 17 | "jcr:content": { 18 | "jcr:primaryType":"cq:PageContent" 19 | } 20 | } 21 | }, 22 | "pageD": { 23 | "jcr:primaryType": "cq:Page", 24 | "jcr:content": { 25 | "jcr:primaryType":"cq:PageContent" 26 | } 27 | }, 28 | "pageE": { 29 | "jcr:primaryType": "cq:Page", 30 | "jcr:content": { 31 | "jcr:primaryType": "cq:PageContent", 32 | "cq:conf": "/conf/test-config" 33 | } 34 | }, 35 | "test": { 36 | "jcr:primaryType": "nt:unstructured", 37 | "cq:graphqlClient": "default" 38 | } 39 | } -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/test/resources/sample-graphql-request.json: -------------------------------------------------------------------------------- 1 | {"query":"{dummy-é}","operationName":"customOperation","variables":{"variableName":"variableValue"}} -------------------------------------------------------------------------------- /src/test/resources/sample-graphql-response.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": { 3 | "text": "Some text", 4 | "count": 42 5 | }, 6 | "errors": [ 7 | { 8 | "message": "Error message" 9 | } 10 | ] 11 | } --------------------------------------------------------------------------------