├── secring.gpg.enc ├── mocca-resilience4j ├── src │ ├── test │ │ ├── resources │ │ │ └── mockito-extensions │ │ │ │ └── org.mockito.plugins.MockMaker │ │ └── java │ │ │ └── com │ │ │ └── paypal │ │ │ └── mocca │ │ │ └── client │ │ │ └── MoccaResilience4jTest.java │ └── main │ │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ ├── MoccaResilience4jFeignDecorators.java │ │ └── MoccaResilience4j.java └── build.gradle ├── mocca-functional-tests ├── src │ ├── main │ │ ├── resources │ │ │ └── schema │ │ │ │ ├── query.graphqls │ │ │ │ ├── author.graphqls │ │ │ │ ├── book.graphqls │ │ │ │ └── mutation.graphqls │ │ └── java │ │ │ └── com │ │ │ └── paypal │ │ │ └── mocca │ │ │ ├── resolvers │ │ │ ├── QueryResolver.java │ │ │ └── MutationResolver.java │ │ │ ├── repository │ │ │ ├── AuthorRepository.java │ │ │ └── BookRepository.java │ │ │ └── server │ │ │ ├── GraphQLRequestBody.java │ │ │ ├── GraphQLFactory.java │ │ │ └── GraphQLEndpoint.java │ └── test │ │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── functional │ │ ├── AsyncBooksAppClient.java │ │ ├── BooksAppClient.java │ │ ├── MoccaMutationTest.java │ │ ├── AbstractFunctionalTests.java │ │ └── MoccaQueryTest.java └── build.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── docs ├── img │ └── logo │ │ ├── mocca_logo_horizontal.png │ │ └── mocca_logo_horizontal_lightbluebackground.png ├── CONTRIBUTING.md └── RELEASE_NOTES.md ├── mocca-http-client-tests ├── README.md ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ └── BasicMoccaHttpClientTest.java ├── mocca-micrometer ├── build.gradle └── src │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ └── MoccaMicrometerCapability.java ├── mocca-client ├── src │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── paypal │ │ │ └── mocca │ │ │ └── client │ │ │ ├── SampleEnum.java │ │ │ ├── PoorFeignCapability.java │ │ │ ├── sample │ │ │ ├── DynamicHeaderClient.java │ │ │ ├── SampleResponseDTO.java │ │ │ ├── AsyncSampleClient.java │ │ │ ├── SampleRequestDTO.java │ │ │ ├── CyclePojo.java │ │ │ ├── ValidatedRequestDTO.java │ │ │ ├── ComplexSampleType.java │ │ │ ├── SampleClient.java │ │ │ ├── SuperComplexResponseType.java │ │ │ └── SuperComplexSampleType.java │ │ │ ├── MoccaDynamicHeaderTest.java │ │ │ ├── MoccaExceptionHandlerTest.java │ │ │ ├── MoccaClientMutationTest.java │ │ │ ├── MoccaDeserializerTest.java │ │ │ └── MoccaClientBuilderTest.java │ └── main │ │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ ├── annotation │ │ ├── package-info.java │ │ ├── RequestHeaderParam.java │ │ ├── Query.java │ │ ├── Mutation.java │ │ ├── RequestHeader.java │ │ ├── SelectionSet.java │ │ └── Var.java │ │ ├── package-info.java │ │ ├── MoccaException.java │ │ ├── MoccaCapability.java │ │ ├── OperationType.java │ │ ├── MoccaFeignInvocationHandlerFactory.java │ │ ├── Arguments.java │ │ ├── MoccaHttpClient.java │ │ ├── MoccaResiliency.java │ │ ├── MoccaAsyncHttpClient.java │ │ ├── MoccaFeignDecoder.java │ │ ├── MoccaDefaultHttpClient.java │ │ ├── MoccaExecutorHttpClient.java │ │ ├── MoccaExceptionHandler.java │ │ ├── MoccaReflection.java │ │ ├── MoccaDeserializer.java │ │ ├── MoccaFeignContract.java │ │ └── MoccaFeignEncoder.java └── build.gradle ├── mocca-hc5 ├── build.gradle └── src │ ├── test │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ └── BasicTest.java │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ ├── MoccaApache5Client.java │ └── MoccaAsyncApache5Client.java ├── mocca-apache ├── build.gradle └── src │ ├── test │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ └── BasicTest.java │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ └── MoccaApacheClient.java ├── mocca-google ├── build.gradle └── src │ ├── test │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ └── BasicTest.java │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ └── MoccaGoogleHttpClient.java ├── mocca-okhttp ├── build.gradle └── src │ ├── test │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ └── BasicTest.java │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ └── MoccaOkHttpClient.java ├── mocca-http2 ├── build.gradle └── src │ ├── test │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ └── BasicTest.java │ └── main │ └── java │ └── com │ └── paypal │ └── mocca │ └── client │ └── MoccaHttp2Client.java ├── settings.gradle ├── .gitignore ├── mocca-jaxrs2 ├── src │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── paypal │ │ │ └── mocca │ │ │ └── client │ │ │ └── BasicTest.java │ └── main │ │ └── java │ │ └── com │ │ └── paypal │ │ └── mocca │ │ └── client │ │ └── MoccaJaxrsClient.java └── build.gradle ├── .github └── workflows │ ├── gradle.yml │ └── gradle-publish.yml ├── LICENSE.txt ├── release_steps.md ├── README.md ├── gradlew.bat └── gradlew /secring.gpg.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal/mocca/HEAD/secring.gpg.enc -------------------------------------------------------------------------------- /mocca-resilience4j/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/resources/schema/query.graphqls: -------------------------------------------------------------------------------- 1 | type Query { 2 | books: [Book!] 3 | } 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal/mocca/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/img/logo/mocca_logo_horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal/mocca/HEAD/docs/img/logo/mocca_logo_horizontal.png -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/resources/schema/author.graphqls: -------------------------------------------------------------------------------- 1 | type Author { 2 | id: Int! 3 | name: String! 4 | } 5 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/resources/schema/book.graphqls: -------------------------------------------------------------------------------- 1 | type Book { 2 | id: Int! 3 | name: String! 4 | authorId: Int! 5 | } 6 | -------------------------------------------------------------------------------- /docs/img/logo/mocca_logo_horizontal_lightbluebackground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/paypal/mocca/HEAD/docs/img/logo/mocca_logo_horizontal_lightbluebackground.png -------------------------------------------------------------------------------- /mocca-http-client-tests/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | This module is a common test bed meant to be used by implementations of 4 | `com.paypal.mocca.client.MoccaHttpClient`. -------------------------------------------------------------------------------- /mocca-micrometer/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_micrometer 4 | api lib.micrometer 5 | } -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/resources/schema/mutation.graphqls: -------------------------------------------------------------------------------- 1 | type Mutation { 2 | addAuthor(name: String) : Author 3 | addBook(name: String, authorId : Int) :Book 4 | } 5 | -------------------------------------------------------------------------------- /mocca-http-client-tests/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.jetty_server, 4 | lib.testng 5 | } 6 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/SampleEnum.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | public enum SampleEnum { 4 | Sample1, 5 | Sample2, 6 | Sample3; 7 | } 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocca annotations necessary to define client APIs 3 | * 4 | * @since 0.0.1 5 | * @author fabiocarvalho777@gmail.com 6 | */ 7 | package com.paypal.mocca.client.annotation; -------------------------------------------------------------------------------- /mocca-hc5/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_hc5 4 | api lib.hc5_client 5 | 6 | testImplementation project(':mocca-http-client-tests'), 7 | lib.testng 8 | } -------------------------------------------------------------------------------- /mocca-apache/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_apache 4 | api lib.apache_client 5 | 6 | testImplementation project(':mocca-http-client-tests'), 7 | lib.testng 8 | } -------------------------------------------------------------------------------- /mocca-google/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_google 4 | api lib.google_client 5 | 6 | testImplementation project(':mocca-http-client-tests'), 7 | lib.testng 8 | } -------------------------------------------------------------------------------- /mocca-okhttp/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_okhttp 4 | api lib.okhttp_client 5 | 6 | 7 | testImplementation project(':mocca-http-client-tests'), 8 | lib.testng 9 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Mocca client builder and basic configuration. {@link com.paypal.mocca.client.MoccaClient} is a good place to start. 3 | * 4 | * @since 0.0.1 5 | * @author fabiocarvalho777@gmail.com 6 | */ 7 | package com.paypal.mocca.client; -------------------------------------------------------------------------------- /mocca-http2/build.gradle: -------------------------------------------------------------------------------- 1 | sourceCompatibility = 1.11 2 | targetCompatibility = 1.11 3 | 4 | dependencies { 5 | implementation project(':mocca-client'), 6 | lib.feign_java11 7 | 8 | testImplementation project(':mocca-http-client-tests'), 9 | lib.testng 10 | } -------------------------------------------------------------------------------- /mocca-okhttp/src/test/java/com/paypal/mocca/client/BasicTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | @Test 6 | public class BasicTest extends BasicMoccaHttpClientTest { 7 | public BasicTest() { 8 | super(new MoccaOkHttpClient()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'mocca' 2 | 3 | include 'mocca-client' 4 | include 'mocca-http-client-tests' 5 | include 'mocca-functional-tests' 6 | include 'mocca-apache' 7 | include 'mocca-google' 8 | include 'mocca-hc5' 9 | include 'mocca-http2' 10 | include 'mocca-jaxrs2' 11 | include 'mocca-okhttp' 12 | include 'mocca-micrometer' 13 | include 'mocca-resilience4j' 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | # Package Files # 4 | *.jar 5 | 6 | # Gradle specific # 7 | .gradle/ 8 | build/ 9 | out/ 10 | !gradle/wrapper/gradle-wrapper.jar 11 | 12 | # IDE Files # 13 | .classpath 14 | .project 15 | .settings/ 16 | .idea/ 17 | *.iml 18 | 19 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 20 | hs_err_pid* 21 | 22 | .DS_Store -------------------------------------------------------------------------------- /mocca-http2/src/test/java/com/paypal/mocca/client/BasicTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | import java.net.http.HttpClient; 6 | 7 | @Test 8 | public class BasicTest extends BasicMoccaHttpClientTest { 9 | public BasicTest() { 10 | super(new MoccaHttp2Client(HttpClient.newBuilder().build())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mocca-jaxrs2/src/test/java/com/paypal/mocca/client/BasicTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import org.testng.annotations.Test; 4 | 5 | import javax.ws.rs.client.ClientBuilder; 6 | 7 | @Test 8 | public class BasicTest extends BasicMoccaHttpClientTest { 9 | public BasicTest() { 10 | super(new MoccaJaxrsClient(ClientBuilder.newClient())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /mocca-apache/src/test/java/com/paypal/mocca/client/BasicTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import org.apache.http.impl.client.HttpClientBuilder; 4 | import org.testng.annotations.Test; 5 | 6 | @Test 7 | public class BasicTest extends BasicMoccaHttpClientTest { 8 | public BasicTest() { 9 | super(new MoccaApacheClient(HttpClientBuilder.create().build())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mocca-google/src/test/java/com/paypal/mocca/client/BasicTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.google.api.client.http.javanet.NetHttpTransport; 4 | import org.testng.annotations.Test; 5 | 6 | @Test 7 | public class BasicTest extends BasicMoccaHttpClientTest { 8 | public BasicTest() { 9 | super(new MoccaGoogleHttpClient(new NetHttpTransport())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mocca-jaxrs2/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_jaxrs2, 4 | lib.slf4j_api 5 | api lib.jaxrs2 6 | 7 | testImplementation project(':mocca-http-client-tests'), 8 | lib.testng, 9 | lib.jersey_client, 10 | lib.jersey_hk2 11 | } 12 | -------------------------------------------------------------------------------- /mocca-hc5/src/test/java/com/paypal/mocca/client/BasicTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 4 | import org.testng.annotations.Test; 5 | 6 | @Test 7 | public class BasicTest extends BasicMoccaHttpClientTest { 8 | public BasicTest() { 9 | super(new MoccaApache5Client(HttpClientBuilder.create().build())); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/PoorFeignCapability.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.Client; 4 | 5 | public class PoorFeignCapability implements feign.Capability { 6 | static final String errorMessage = "I have nothing to give:("; 7 | @Override 8 | public Client enrich(Client client) { 9 | throw new RuntimeException(errorMessage); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /mocca-resilience4j/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation project(':mocca-client'), 3 | lib.feign_core, 4 | lib.resilience4j_feign 5 | 6 | api lib.resilience4j_retry, 7 | lib.resilience4j_circuitbreaker, 8 | lib.resilience4j_ratelimiter, 9 | lib.resilience4j_bulkhead 10 | 11 | testImplementation lib.testng, 12 | lib.mockito_core 13 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/DynamicHeaderClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | 4 | import com.paypal.mocca.client.MoccaClient; 5 | import com.paypal.mocca.client.annotation.Query; 6 | import com.paypal.mocca.client.annotation.RequestHeader; 7 | 8 | @RequestHeader("classheader: { classvalue }") 9 | public interface DynamicHeaderClient extends MoccaClient { 10 | 11 | @Query 12 | SampleResponseDTO getOneSample(String variables); 13 | } 14 | -------------------------------------------------------------------------------- /mocca-client/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | implementation lib.slf4j_api, 3 | lib.feign_core, 4 | lib.jackson_databind, 5 | lib.jackson_datatype_jsr301, 6 | lib.jackson_modules_java8 7 | 8 | api lib.jakarta_validation_api 9 | 10 | testImplementation lib.testng, 11 | lib.slf4j_simple, 12 | lib.wiremock, 13 | lib.hibernate_validator, 14 | lib.jakarta_el 15 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaException.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | /** 4 | * Unchecked exception thrown when an 5 | * unexpected error happened during a Mocca call. 6 | * 7 | * @author fabiocarvalho777@gmail.com 8 | */ 9 | public class MoccaException extends RuntimeException { 10 | 11 | public MoccaException(String message) { 12 | super(message); 13 | } 14 | 15 | public MoccaException(String message, Throwable cause) { 16 | super(message, cause); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/SampleResponseDTO.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | public class SampleResponseDTO { 4 | 5 | private String foo; 6 | private String bar; 7 | 8 | public void setFoo(String foo) { 9 | this.foo = foo; 10 | } 11 | 12 | public void setBar(String bar) { 13 | this.bar = bar; 14 | } 15 | 16 | public String getFoo() { 17 | return foo; 18 | } 19 | 20 | public String getBar() { 21 | return bar; 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Push build test 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | - name: Build with Gradle 25 | uses: gradle/gradle-build-action@v2.4.2 26 | with: 27 | arguments: build -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/AsyncSampleClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | import com.paypal.mocca.client.MoccaClient; 4 | import com.paypal.mocca.client.annotation.Query; 5 | import com.paypal.mocca.client.annotation.RequestHeader; 6 | import com.paypal.mocca.client.annotation.Var; 7 | 8 | import java.util.concurrent.CompletableFuture; 9 | 10 | @RequestHeader("classheader: classvalue") 11 | public interface AsyncSampleClient extends MoccaClient { 12 | 13 | @Query 14 | CompletableFuture getOneSample(@Var("foo") String foo, @Var("bar") String bar); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/test/java/com/paypal/mocca/functional/AsyncBooksAppClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.functional; 2 | 3 | import com.paypal.mocca.client.MoccaClient; 4 | import com.paypal.mocca.client.annotation.Mutation; 5 | import com.paypal.mocca.client.annotation.Query; 6 | import com.paypal.mocca.client.annotation.Var; 7 | import com.paypal.mocca.client.model.Author; 8 | import com.paypal.mocca.client.model.Book; 9 | 10 | import java.util.List; 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | public interface AsyncBooksAppClient extends MoccaClient { 14 | 15 | @Query 16 | CompletableFuture> books(); 17 | 18 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/MoccaDynamicHeaderTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.sample.DynamicHeaderClient; 4 | import org.testng.annotations.Test; 5 | 6 | /** 7 | * Tests to verify dynamic header cannot be added at class level 8 | */ 9 | public class MoccaDynamicHeaderTest { 10 | 11 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "(Header value:\\{ classvalue } at class level cannot be dynamic)") 12 | public void verifyDynamicHeader() { 13 | DynamicHeaderClient client = MoccaClient.Builder.sync("dummyurl").build(DynamicHeaderClient.class); 14 | String queryVariables = "foo: \"zoo\", bar: \"car\""; 15 | client.getOneSample(queryVariables); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/SampleRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | public class SampleRequestDTO { 4 | 5 | private String foo; 6 | private String bar; 7 | 8 | public SampleRequestDTO() { 9 | } 10 | 11 | public SampleRequestDTO(String foo, String bar) { 12 | this.foo = foo; 13 | this.bar = bar; 14 | } 15 | 16 | public SampleRequestDTO setFoo(String foo) { 17 | this.foo = foo; 18 | return this; 19 | } 20 | 21 | public SampleRequestDTO setBar(String bar) { 22 | this.bar = bar; 23 | return this; 24 | } 25 | 26 | public String getFoo() { 27 | return foo; 28 | } 29 | 30 | public String getBar() { 31 | return bar; 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /mocca-functional-tests/src/test/java/com/paypal/mocca/functional/BooksAppClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.functional; 2 | 3 | import com.paypal.mocca.client.MoccaClient; 4 | import com.paypal.mocca.client.annotation.Mutation; 5 | import com.paypal.mocca.client.annotation.Query; 6 | import com.paypal.mocca.client.annotation.Var; 7 | import com.paypal.mocca.client.model.Author; 8 | import com.paypal.mocca.client.model.Book; 9 | 10 | import javax.validation.constraints.NotNull; 11 | import java.util.List; 12 | 13 | public interface BooksAppClient extends MoccaClient { 14 | 15 | @Query 16 | List books(); 17 | 18 | @Mutation 19 | Author addAuthor(@NotNull @Var("name") String authorName); 20 | 21 | @Mutation 22 | Book addBook(@Var("name") String name, @Var("authorId") int authorId); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaCapability.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | /** 4 | * {@link MoccaClient} supports various capabilities (e.g. metrics collection). 5 | * They are represented by extensions of this class and are most often found in 6 | * optional libraries. You register a capability through the 'addCapability' 7 | * method that you get from MoccaClient's builders. 8 | * 9 | * @author crankydillo@gmail.com 10 | */ 11 | abstract class MoccaCapability { 12 | private final feign.Capability feignCapability; 13 | 14 | protected MoccaCapability(feign.Capability feignCapability) { 15 | this.feignCapability = 16 | Arguments.requireNonNull(feignCapability, "Feign capability cannot be null"); 17 | } 18 | 19 | feign.Capability getFeignCapability() { 20 | return feignCapability; 21 | } 22 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/CyclePojo.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | public class CyclePojo { 4 | 5 | private int number; 6 | private String color; 7 | private CyclePojo cyclePojo; 8 | 9 | public CyclePojo setNumber(int number) { 10 | this.number = number; 11 | return this; 12 | } 13 | 14 | public CyclePojo setColor(String color) { 15 | this.color = color; 16 | return this; 17 | } 18 | 19 | public CyclePojo setCyclePojo(CyclePojo cyclePojo) { 20 | this.cyclePojo = cyclePojo; 21 | return this; 22 | } 23 | 24 | public int getNumber() { 25 | return number; 26 | } 27 | 28 | public String getColor() { 29 | return color; 30 | } 31 | 32 | public CyclePojo getCyclePojo() { 33 | return cyclePojo; 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/ValidatedRequestDTO.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | import org.hibernate.validator.constraints.Length; 4 | 5 | import javax.validation.constraints.NotNull; 6 | 7 | public class ValidatedRequestDTO { 8 | 9 | @NotNull 10 | private String foo; 11 | 12 | @Length(max=5) 13 | private String bar; 14 | 15 | public ValidatedRequestDTO() { 16 | } 17 | 18 | public ValidatedRequestDTO(String foo, String bar) { 19 | this.foo = foo; 20 | this.bar = bar; 21 | } 22 | 23 | public ValidatedRequestDTO setFoo(String foo) { 24 | this.foo = foo; 25 | return this; 26 | } 27 | 28 | public ValidatedRequestDTO setBar(String bar) { 29 | this.bar = bar; 30 | return this; 31 | } 32 | 33 | public String getFoo() { 34 | return foo; 35 | } 36 | 37 | public String getBar() { 38 | return bar; 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/RequestHeaderParam.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Annotation used to set HTTP header dynamic values. 10 | * It can only be used at the method level. 11 | * See documentation at {@link RequestHeader} for further information. 12 | * 13 | * @see RequestHeader 14 | * @author abprabhakar@paypal.com 15 | */ 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target({ElementType.PARAMETER}) 18 | public @interface RequestHeaderParam { 19 | 20 | /** 21 | * Sets the name of the header value placeholder defined in the {@link RequestHeader} annotation. 22 | * See documentation at {@link RequestHeader} for further information. 23 | * @return the name of the header value placeholder 24 | */ 25 | String value(); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2021-22 PayPal 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/OperationType.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import java.lang.annotation.Annotation; 4 | 5 | /** 6 | * Enumeration representing each supported GraphQL operation. 7 | * This enumeration is internal purposes only, bot being 8 | * part of Mocca API. 9 | * 10 | * @author fabiocarvalho777@gmail.com 11 | */ 12 | enum OperationType { 13 | 14 | Query("query"), 15 | Mutation("mutation"); 16 | 17 | private final String value; 18 | 19 | OperationType(String value) { 20 | this.value = value; 21 | } 22 | 23 | static OperationType valueOf(Annotation operationAnnotation) { 24 | if (operationAnnotation instanceof com.paypal.mocca.client.annotation.Query) return Query; 25 | if (operationAnnotation instanceof com.paypal.mocca.client.annotation.Mutation) return Mutation; 26 | throw new IllegalArgumentException("Unsupported annotation: " + operationAnnotation.getClass().getName()); 27 | } 28 | 29 | String getValue() { 30 | return value; 31 | } 32 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaFeignInvocationHandlerFactory.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.FeignException; 4 | import feign.InvocationHandlerFactory; 5 | import feign.Target; 6 | 7 | import java.lang.reflect.InvocationHandler; 8 | import java.lang.reflect.Method; 9 | import java.util.Map; 10 | 11 | /** 12 | * This class allows Mocca to hide Feign exception types from applications 13 | * when using a custom Feign invocation handler factory is possible 14 | * 15 | * @author facarvalho, crankydillo@gmail.com 16 | */ 17 | class MoccaFeignInvocationHandlerFactory implements InvocationHandlerFactory { 18 | 19 | private final InvocationHandlerFactory delegate = new InvocationHandlerFactory.Default(); 20 | 21 | @Override 22 | public InvocationHandler create(Target target, Map dispatch) { 23 | return (proxy, method, args) -> { 24 | try { 25 | return delegate.create(target, dispatch).invoke(proxy, method, args); 26 | } catch (FeignException e) { 27 | throw MoccaExceptionHandler.handleException(e); 28 | } 29 | }; 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /mocca-okhttp/src/main/java/com/paypal/mocca/client/MoccaOkHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | /** 4 | * Mocca OkHttp client. In order to use a OkHttp client with Mocca, 5 | * create a new instance of this class and pass it to Mocca builder. 6 | *
7 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#client(MoccaHttpClient)} for further information and code example. 8 | * 9 | * @author fabiocarvalho777@gmail.com 10 | */ 11 | final public class MoccaOkHttpClient extends MoccaHttpClient { 12 | 13 | /** 14 | * Creates a new Mocca OkHttp client using 15 | * default OkHttp client configuration 16 | */ 17 | public MoccaOkHttpClient() { 18 | this(new okhttp3.OkHttpClient()); 19 | } 20 | 21 | /** 22 | * Creates a new Mocca OkHttp client using 23 | * a pre-instantiated OkHttp client with user 24 | * defined configuration 25 | * 26 | * @param okHttpClient a pre-instantiated OkHttp client 27 | * with user defined configuration 28 | */ 29 | public MoccaOkHttpClient(okhttp3.OkHttpClient okHttpClient) { 30 | super(new feign.okhttp.OkHttpClient(okHttpClient)); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /mocca-functional-tests/src/test/java/com/paypal/mocca/functional/MoccaMutationTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.functional; 2 | 3 | import com.paypal.mocca.client.model.Author; 4 | import com.paypal.mocca.client.model.Book; 5 | import org.testng.annotations.Test; 6 | 7 | import static org.testng.Assert.assertEquals; 8 | import static org.testng.Assert.assertNotNull; 9 | 10 | /** 11 | * Basic mutation tests 12 | */ 13 | public class MoccaMutationTest extends AbstractFunctionalTests { 14 | 15 | /** 16 | * Test basic graphql mutations on books 17 | * 18 | * @throws Exception if request cannot be made 19 | */ 20 | @Test 21 | public void testBasicMutations() throws Exception { 22 | 23 | //Add Author first 24 | Author author = client.addAuthor("mocca"); 25 | 26 | assertNotNull(author); 27 | assertNotNull(author.getId()); 28 | assertEquals(author.getName(), "mocca"); 29 | 30 | //Add Book 31 | Book book = client.addBook("moccaBook", author.getId()); 32 | assertNotNull(book); 33 | assertNotNull(book.getId()); 34 | assertEquals(book.getAuthorId(), author.getId()); 35 | assertEquals(book.getName(), "moccaBook"); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/resolvers/QueryResolver.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.resolvers; 2 | 3 | import com.paypal.mocca.model.Book; 4 | import com.paypal.mocca.repository.AuthorRepository; 5 | import com.paypal.mocca.repository.BookRepository; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Query resolver for application 11 | */ 12 | public class QueryResolver implements com.paypal.mocca.api.QueryResolver { 13 | 14 | /** 15 | * Author repository 16 | */ 17 | private final AuthorRepository authorRepository; 18 | /** 19 | * Book repository 20 | */ 21 | private final BookRepository bookRepository; 22 | 23 | /** 24 | * Injects book repository and author repository 25 | * 26 | * @param bookRepository BookRepository 27 | * @param authorRepository AuthorRepository 28 | */ 29 | public QueryResolver(BookRepository bookRepository, AuthorRepository authorRepository) { 30 | this.authorRepository = authorRepository; 31 | this.bookRepository = bookRepository; 32 | } 33 | 34 | /** 35 | * Get list of all books on store 36 | * 37 | * @return books list 38 | */ 39 | @Override 40 | public List books() { 41 | return this.bookRepository.getAll(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /mocca-apache/src/main/java/com/paypal/mocca/client/MoccaApacheClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.httpclient.ApacheHttpClient; 4 | import org.apache.http.client.HttpClient; 5 | import org.apache.http.impl.client.HttpClientBuilder; 6 | 7 | /** 8 | * Mocca Apache HTTP client. In order to use a Apache HTTP client with Mocca, 9 | * create a new instance of this class and pass it to Mocca builder. 10 | *
11 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#client(MoccaHttpClient)} for further information and code example. 12 | * 13 | * @author fabiocarvalho777@gmail.com 14 | */ 15 | final public class MoccaApacheClient extends MoccaHttpClient { 16 | 17 | /** 18 | * Creates a new Mocca Apache HTTP client using 19 | * default Apache HTTP client configuration 20 | */ 21 | public MoccaApacheClient() { 22 | this(HttpClientBuilder.create().build()); 23 | } 24 | 25 | /** 26 | * Creates a new Mocca Apache HTTP client using 27 | * a pre-instantiated Apache HTTP client with user 28 | * defined configuration 29 | * 30 | * @param httpClient a pre-instantiated Apache HTTP client 31 | * with user defined configuration 32 | */ 33 | public MoccaApacheClient(HttpClient httpClient) { 34 | super(new ApacheHttpClient(httpClient)); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /mocca-google/src/main/java/com/paypal/mocca/client/MoccaGoogleHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.google.api.client.http.HttpTransport; 4 | import com.google.api.client.http.javanet.NetHttpTransport; 5 | 6 | /** 7 | * Mocca Google HTTP client. In order to use a Google HTTP client with Mocca, 8 | * create a new instance of this class and pass it to Mocca builder. 9 | *
10 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#client(MoccaHttpClient)} for further information and code example. 11 | * 12 | * @author fabiocarvalho777@gmail.com 13 | */ 14 | final public class MoccaGoogleHttpClient extends MoccaHttpClient { 15 | 16 | /** 17 | * Creates a new Mocca Google HTTP client using 18 | * default Google HTTP client configuration 19 | */ 20 | public MoccaGoogleHttpClient() { 21 | this(new NetHttpTransport()); 22 | } 23 | 24 | /** 25 | * Creates a new Mocca Google HTTP client using 26 | * a pre-instantiated Google HTTP client with user 27 | * defined configuration 28 | * 29 | * @param httpTransport a pre-instantiated Google HTTP client 30 | * with user defined configuration 31 | */ 32 | public MoccaGoogleHttpClient(HttpTransport httpTransport) { 33 | super(new feign.googlehttpclient.GoogleHttpClient(httpTransport)); 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /mocca-hc5/src/main/java/com/paypal/mocca/client/MoccaApache5Client.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.hc5.ApacheHttp5Client; 4 | import org.apache.hc.client5.http.classic.HttpClient; 5 | import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; 6 | 7 | /** 8 | * Mocca Apache 5 HTTP client. In order to use a Apache 5 HTTP client with Mocca, 9 | * create a new instance of this class and pass it to Mocca builder. 10 | *
11 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#client(MoccaHttpClient)} for further information and code example. 12 | * 13 | * @author fabiocarvalho777@gmail.com 14 | */ 15 | final public class MoccaApache5Client extends MoccaHttpClient { 16 | 17 | /** 18 | * Creates a new Mocca Apache 5 HTTP client using 19 | * default Apache 5 HTTP client configuration 20 | */ 21 | public MoccaApache5Client() { 22 | this(HttpClientBuilder.create().build()); 23 | } 24 | 25 | /** 26 | * Creates a new Mocca Apache 5 HTTP client using 27 | * a pre-instantiated Apache 5 HTTP client with user 28 | * defined configuration 29 | * 30 | * @param httpClient a pre-instantiated Apache 5 HTTP client 31 | * with user defined configuration 32 | */ 33 | public MoccaApache5Client(HttpClient httpClient) { 34 | super(new ApacheHttp5Client(httpClient)); 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /mocca-http2/src/main/java/com/paypal/mocca/client/MoccaHttp2Client.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.http2client.Http2Client; 4 | 5 | import java.net.http.HttpClient; 6 | 7 | /** 8 | * Mocca Java 11 HTTP 2 client. In order to use a Java 11 HTTP 2 client with Mocca, 9 | * create a new instance of this class and pass it to Mocca builder. 10 | *
11 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#client(MoccaHttpClient)} for further information and code example. 12 | * 13 | * @author fabiocarvalho777@gmail.com 14 | */ 15 | final public class MoccaHttp2Client extends MoccaHttpClient { 16 | 17 | /** 18 | * Creates a new Mocca Java 11 HTTP 2 client using 19 | * default Java 11 HTTP 2 client configuration 20 | */ 21 | public MoccaHttp2Client() { 22 | this(HttpClient.newBuilder() 23 | .followRedirects(HttpClient.Redirect.ALWAYS) 24 | .version(HttpClient.Version.HTTP_2) 25 | .build()); 26 | } 27 | 28 | /** 29 | * Creates a new Mocca Java 11 HTTP 2 client using 30 | * a pre-instantiated Java 11 HTTP 2 client with user 31 | * defined configuration 32 | * 33 | * @param httpClient a pre-instantiated Java 11 HTTP 2 client 34 | * with user defined configuration 35 | */ 36 | public MoccaHttp2Client(HttpClient httpClient) { 37 | super(new Http2Client(httpClient)); 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/repository/AuthorRepository.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.repository; 2 | 3 | import com.paypal.mocca.model.Author; 4 | 5 | import java.util.HashMap; 6 | import java.util.Map; 7 | 8 | /** 9 | * Author repository which caches author data 10 | */ 11 | public class AuthorRepository { 12 | /** 13 | * Author map 14 | */ 15 | private static final Map AUTHOR_MAP = new HashMap<>(); 16 | 17 | /** 18 | * Constructor which loads initial data 19 | */ 20 | public AuthorRepository() { 21 | Author author = new Author(); 22 | author.setId(1); 23 | author.setName("Test1"); 24 | AUTHOR_MAP.put(1, author); 25 | } 26 | 27 | /** 28 | * Save author to store. Calculates id based on map size 29 | * 30 | * @param name author name 31 | * @return Saved author with id updated 32 | */ 33 | public Author save(String name) { 34 | final int id = AUTHOR_MAP.size() + 1; 35 | Author author = new Author(); 36 | author.setId(id); 37 | author.setName(name); 38 | AUTHOR_MAP.put(id, author); 39 | return author; 40 | } 41 | 42 | /** 43 | * Get given author by id. Null author does not exists. 44 | * 45 | * @param authorId author id 46 | * @return author 47 | */ 48 | public Author get(int authorId) { 49 | return AUTHOR_MAP.get(authorId); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/Arguments.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | /** 4 | * Utility methods for working consistently with arguments. 5 | * Similar to {@link java.util.Objects}. It also has similarities 6 | * to Guava's Preconditions. The functions almost always throw 7 | * {@link IllegalArgumentException} when requirements are not met. 8 | * 9 | * @author crankydillo@gmail.com 10 | */ 11 | final class Arguments { 12 | private Arguments() {} 13 | 14 | static void require(final boolean assertion) { 15 | if (!assertion) { 16 | throw new IllegalArgumentException(); 17 | } 18 | } 19 | 20 | static void require(final boolean assertion, final String message) { 21 | if (!assertion) { 22 | throw new IllegalArgumentException(message); 23 | } 24 | } 25 | 26 | static T requireNonNull(final T t) { 27 | return require(t, t != null); 28 | } 29 | 30 | static T requireNonNull(final T t, final String message) { 31 | return require(t, t != null, message); 32 | } 33 | 34 | static T require(T t, final boolean assertion) { 35 | if (!assertion) { 36 | throw new IllegalArgumentException(); 37 | } 38 | return t; 39 | } 40 | 41 | static T require(T t, final boolean assertion, final String message) { 42 | if (!assertion) { 43 | throw new IllegalArgumentException(message); 44 | } 45 | return t; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /mocca-resilience4j/src/main/java/com/paypal/mocca/client/MoccaResilience4jFeignDecorators.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.FeignException; 4 | import feign.InvocationHandlerFactory; 5 | import feign.Target; 6 | import io.github.resilience4j.feign.FeignDecorator; 7 | import io.github.resilience4j.feign.FeignDecorators; 8 | import io.vavr.CheckedFunction1; 9 | 10 | import java.lang.reflect.Method; 11 | 12 | /** 13 | * This class allows Mocca to hide Feign exception types from applications 14 | * when using Resilience4j 15 | * 16 | * @author facarvalho, crankydillo@gmail.com 17 | * @author facarvalho 18 | */ 19 | class MoccaResilience4jFeignDecorators implements FeignDecorator { 20 | 21 | private FeignDecorators feignDecorators; 22 | 23 | MoccaResilience4jFeignDecorators(FeignDecorators feignDecorators) { 24 | this.feignDecorators = feignDecorators; 25 | } 26 | 27 | @Override 28 | public CheckedFunction1 decorate(CheckedFunction1 invocationCall, Method method, InvocationHandlerFactory.MethodHandler methodHandler, Target target) { 29 | return (clientMethodParameters) -> { 30 | try { 31 | return feignDecorators 32 | .decorate(invocationCall, method, methodHandler, target) 33 | .apply(clientMethodParameters); 34 | } catch (FeignException e) { 35 | throw MoccaExceptionHandler.handleException(e); 36 | } 37 | }; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/server/GraphQLRequestBody.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.server; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * Representation for GraphQL request body 7 | */ 8 | public class GraphQLRequestBody { 9 | 10 | /** 11 | * GraphQL Query 12 | */ 13 | private String query; 14 | /** 15 | * GraphQL Operation Name 16 | */ 17 | private String operationName; 18 | /** 19 | * GraphQL variables 20 | */ 21 | private Map variables; 22 | 23 | /** 24 | * Get query string 25 | * 26 | * @return query string 27 | */ 28 | public String getQuery() { 29 | return query; 30 | } 31 | 32 | /** 33 | * Set query string 34 | * 35 | * @param query 36 | * query string 37 | */ 38 | public void setQuery(String query) { 39 | this.query = query; 40 | } 41 | 42 | /** 43 | * Get GraphQL operation name 44 | * 45 | * @return operation name 46 | */ 47 | public String getOperationName() { 48 | return operationName; 49 | } 50 | 51 | /** 52 | * Set operation name 53 | * 54 | * @param operationName 55 | * operation name 56 | */ 57 | public void setOperationName(String operationName) { 58 | this.operationName = operationName; 59 | } 60 | 61 | /** 62 | * Get Variables 63 | * 64 | * @return variables 65 | */ 66 | public Map getVariables() { 67 | return variables; 68 | } 69 | 70 | /** 71 | * Set query variables 72 | * 73 | * @param variables 74 | * variables 75 | */ 76 | public void setVariables(Map variables) { 77 | this.variables = variables; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.Client; 4 | 5 | /** 6 | * An abstract class representing a synchronous HTTP client supported by Mocca. Subclasses are supposed 7 | * to work as wrappers to HTTP clients, based on composition, and must adhere to 8 | * the following rules: 9 | *
    10 | *
  1. Be delivered on its own module, named with pattern {@code mocca-}
  2. 11 | *
  3. Be packaged at the same package as this abstract class
  4. 12 | *
  5. Be declared as public and final
  6. 13 | *
  7. Offer a public default constructor, providing a default instance of its HTTP client
  8. 14 | *
  9. Offer a public constructor with one argument, allowing users to specify a pre-instantiated custom HTTP client object
  10. 15 | *
16 | * 17 | * @author fabiocarvalho777@gmail.com 18 | */ 19 | abstract class MoccaHttpClient { 20 | 21 | private final Client feignClient; 22 | 23 | protected MoccaHttpClient(Client feignClient) { 24 | this.feignClient = 25 | Arguments.requireNonNull(feignClient, "Feign client cannot be null"); 26 | } 27 | 28 | /** 29 | * Returns a Feign client containing the HTTP synchronous client specified in the subclass, 30 | * to be used in a Mocca builder when creating a Mocca client 31 | * 32 | * @return a Feign client containing the HTTP synchronous client specified in the subclass, 33 | * to be used in a Mocca builder when creating a Mocca client 34 | */ 35 | Client getFeignClient() { 36 | return feignClient; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/Query.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | /** 10 | * Annotation used to define a GraphQL query 11 | * and its configuration in a client API. 12 | *
13 | * See the client API example below. 14 | *

15 |  * import com.paypal.mocca.client.MoccaClient;
16 |  * import com.paypal.mocca.client.annotation.Mutation;
17 |  * import com.paypal.mocca.client.annotation.Query;
18 |  * import com.paypal.mocca.client.annotation.SelectionSet;
19 |  *
20 |  * public interface BooksAppClient extends MoccaClient {
21 |  *
22 |  *     @Query
23 |  *     @SelectionSet("{id, name}")
24 |  *     List<Book> getBooks(String variables);
25 |  *
26 |  *     @Query
27 |  *     Book getBook(long id);
28 |  *
29 |  *     @Mutation
30 |  *     Author addAuthor(@Variable(ignore = "books")Author author);
31 |  *
32 |  *     @Mutation
33 |  *     Book addBook(Book book);
34 |  *
35 |  * }
36 | * @author fabiocarvalho777@gmail.com 37 | */ 38 | @Retention(RUNTIME) 39 | @Target(ElementType.METHOD) 40 | public @interface Query { 41 | 42 | String UNDEFINED = "UNDEFINED"; 43 | 44 | /** 45 | * Used to provide a custom name to the GraphQL query. 46 | * If not set, the operation method name will be used as query name. 47 | * 48 | * @return a custom name set to the GraphQL query 49 | */ 50 | String name() default UNDEFINED; 51 | 52 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/Mutation.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | /** 10 | * Annotation used to define a GraphQL mutation 11 | * and its configuration in a client API. 12 | *
13 | * See the client API example below. 14 | *

15 |  * import com.paypal.mocca.client.MoccaClient;
16 |  * import com.paypal.mocca.client.annotation.Mutation;
17 |  * import com.paypal.mocca.client.annotation.Query;
18 |  * import com.paypal.mocca.client.annotation.SelectionSet;
19 |  *
20 |  * public interface BooksAppClient extends MoccaClient {
21 |  *
22 |  *     @Query
23 |  *     @SelectionSet("{id, name}")
24 |  *     List<Book> getBooks(String variables);
25 |  *
26 |  *     @Query
27 |  *     Book getBook(long id);
28 |  *
29 |  *     @Mutation
30 |  *     Author addAuthor(@Variable(ignore = "books")Author author);
31 |  *
32 |  *     @Mutation
33 |  *     Book addBook(Book book);
34 |  *
35 |  * }
36 | * 37 | * @author fabiocarvalho777@gmail.com 38 | */ 39 | @Retention(RUNTIME) 40 | @Target(ElementType.METHOD) 41 | public @interface Mutation { 42 | 43 | String UNDEFINED = "UNDEFINED"; 44 | 45 | /** 46 | * Used to provide a custom name to the GraphQL mutation. 47 | * If not set, the operation method name will be used as mutation name. 48 | * 49 | * @return a custom name set to the GraphQL mutation 50 | */ 51 | String name() default UNDEFINED; 52 | 53 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaResiliency.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.Feign; 4 | 5 | /** 6 | * This is a mechanism for supporting {@link MoccaClient} resiliency features. Similar to 7 | * {@link MoccaHttpClient}, the actual functionality is delivered via an opt-in additional library, 8 | * which is then registered by using {@link MoccaClient.Builder.SyncBuilder#resiliency(MoccaResiliency)}. 9 | * 10 | * @author crankydillo@gmail.com 11 | */ 12 | abstract class MoccaResiliency { 13 | // FIXME this messiness exists to support resiliency using resilience4j-feign 14 | // which operates at the builder level; however, we want to present it as a 15 | // in a non-coupled way with via our builder.. Ultimately, we believe feign 16 | // will release resilience4j as a feign.Capability and then we can get rid of 17 | // this. An important thing to note is this nasty impl stuff is hidden from 18 | // the user. The main user problem is that if they add multiple of these to 19 | // the builder a runtime exception will be generated. 20 | // 21 | // This can be done as a `MoccaCapability`, but it will make the impl look messier 22 | // and could have some effects like runtime exceptions if more than one builder-based 23 | // capability is added. I had most of that coded and then went this route.. 24 | private final Feign.Builder feignBuilder; 25 | 26 | protected MoccaResiliency(Feign.Builder feignBuilder) { 27 | this.feignBuilder = 28 | Arguments.requireNonNull(feignBuilder, "Feign builder cannot be null"); 29 | } 30 | 31 | Feign.Builder getFeignBuilder() { 32 | return feignBuilder; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaAsyncHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.AsyncClient; 4 | 5 | /** 6 | * An abstract class representing an asynchronous HTTP client supported by Mocca. Subclasses are supposed 7 | * to work as wrappers to asynchronous HTTP clients, based on composition, and must adhere to 8 | * the following rules: 9 | *
    10 | *
  1. Be delivered on its own module, named with pattern {@code mocca-}
  2. 11 | *
  3. Be packaged at the same package as this abstract class
  4. 12 | *
  5. Be declared as public and final
  6. 13 | *
  7. Offer a public default constructor, providing a default instance of its asynchronous HTTP client
  8. 14 | *
  9. Define the generics parameter using the asynchronous HTTP client type
  10. 15 | *
16 | * 17 | * @param the asynchronous HTTP client type 18 | * @author fabiocarvalho777@gmail.com 19 | */ 20 | abstract class MoccaAsyncHttpClient { 21 | 22 | private final AsyncClient feignAsyncClient; 23 | 24 | protected MoccaAsyncHttpClient(AsyncClient feignAsyncClient) { 25 | this.feignAsyncClient = 26 | Arguments.requireNonNull(feignAsyncClient, "Feign async client cannot be null"); 27 | } 28 | 29 | /** 30 | * Returns a Feign client containing the asynchronous HTTP client specified in the subclass, 31 | * to be used in a Mocca builder when creating a Mocca client 32 | * 33 | * @return a Feign client containing the asynchronous HTTP client specified in the subclass, 34 | * to be used in a Mocca builder when creating a Mocca client 35 | */ 36 | AsyncClient getFeignAsyncClient() { 37 | return feignAsyncClient; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaFeignDecoder.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.FeignException; 4 | import feign.Response; 5 | import feign.codec.Decoder; 6 | 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.lang.reflect.Type; 10 | import java.util.Optional; 11 | 12 | import static com.paypal.mocca.client.MoccaReflection.getInnerType; 13 | import static com.paypal.mocca.client.MoccaReflection.isParameterizedType; 14 | 15 | /** 16 | * Mocca Feign decoder, responsible for deserializing the response payload 17 | * 18 | * @author fabiocarvalho777@gmail.com 19 | */ 20 | class MoccaFeignDecoder implements Decoder { 21 | 22 | private final MoccaDeserializer moccaDeserializer = new MoccaDeserializer(); 23 | 24 | @Override 25 | public Object decode(Response response, Type type) throws IOException, FeignException { 26 | if (response.status() != 200) { 27 | throw new MoccaException("Unexpected HTTP response status code: " + response.status()); 28 | } 29 | 30 | final boolean optionalResultType = isParameterizedType(type, Optional.class); 31 | 32 | Optional result; 33 | try (InputStream inputStream = response.body().asInputStream()) { 34 | if (inputStream == null) { 35 | throw new MoccaException("Response does not contain a payload"); 36 | } 37 | 38 | final String operationName = MoccaFeignEncoder.getOperationName(response); 39 | final Type responseType = optionalResultType ? getInnerType(type) : type; 40 | result = moccaDeserializer.deserialize(inputStream, responseType, operationName); 41 | } 42 | 43 | return optionalResultType ? result : result.orElse(null); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /mocca-hc5/src/main/java/com/paypal/mocca/client/MoccaAsyncApache5Client.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.hc5.AsyncApacheHttp5Client; 4 | import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; 5 | import org.apache.hc.client5.http.impl.async.HttpAsyncClients; 6 | import org.apache.hc.client5.http.protocol.HttpClientContext; 7 | 8 | /** 9 | * Mocca Async Apache 5 HTTP client. In order to use a Async Apache 5 HTTP client with Mocca, 10 | * create a new instance of this class and pass it to Mocca builder. 11 | *
12 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.AsyncBuilder#client(MoccaAsyncHttpClient)} for further information and code example. 13 | * 14 | * @author fabiocarvalho777@gmail.com 15 | */ 16 | final public class MoccaAsyncApache5Client extends MoccaAsyncHttpClient { 17 | 18 | /** 19 | * Creates a new Mocca Async Apache 5 HTTP client using 20 | * default Async Apache 5 HTTP client configuration 21 | */ 22 | public MoccaAsyncApache5Client() { 23 | this(createStartedClient()); 24 | } 25 | 26 | private static CloseableHttpAsyncClient createStartedClient() { 27 | final CloseableHttpAsyncClient client = HttpAsyncClients.custom().build(); 28 | client.start(); 29 | return client; 30 | } 31 | 32 | /** 33 | * Creates a new Mocca Async Apache 5 HTTP client using 34 | * a pre-instantiated Async Apache 5 HTTP client with user 35 | * defined configuration 36 | * 37 | * @param httpClient a pre-instantiated Async Apache 5 HTTP client 38 | * with user defined configuration 39 | */ 40 | public MoccaAsyncApache5Client(CloseableHttpAsyncClient httpClient) { 41 | super(new AsyncApacheHttp5Client(httpClient)); 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/resolvers/MutationResolver.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.resolvers; 2 | 3 | import com.paypal.mocca.model.Author; 4 | import com.paypal.mocca.model.Book; 5 | import com.paypal.mocca.repository.AuthorRepository; 6 | import com.paypal.mocca.repository.BookRepository; 7 | 8 | /** 9 | * GraphQL mutation resolver for all mutations 10 | */ 11 | public class MutationResolver implements com.paypal.mocca.api.MutationResolver { 12 | /** 13 | * Book Repository 14 | */ 15 | private final BookRepository bookRepository; 16 | /** 17 | * Author repository 18 | */ 19 | private final AuthorRepository authorRepository; 20 | 21 | /** 22 | * Initialize resolver with both repositories 23 | * 24 | * @param bookRepository book repository 25 | * @param authorRepository author repoisitory 26 | */ 27 | public MutationResolver(BookRepository bookRepository, AuthorRepository authorRepository) { 28 | this.bookRepository = bookRepository; 29 | this.authorRepository = authorRepository; 30 | } 31 | 32 | /** 33 | * Add given book to store 34 | * 35 | * @param name book name 36 | * @param authorId author id 37 | * @return added book with id 38 | */ 39 | @Override 40 | public Book addBook(String name, Integer authorId) { 41 | if (authorRepository.get(authorId) == null) { 42 | throw new RuntimeException("Author does not exists"); 43 | } 44 | return this.bookRepository.save(name, authorId); 45 | } 46 | 47 | /** 48 | * Add author to store 49 | * 50 | * @param name author name 51 | * @return added author with id 52 | */ 53 | @Override 54 | public Author addAuthor(String name) { 55 | return this.authorRepository.save(name); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaDefaultHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import javax.net.ssl.HostnameVerifier; 4 | import javax.net.ssl.SSLSocketFactory; 5 | import java.net.HttpURLConnection; 6 | 7 | /** 8 | * Mocca HTTP client based on {@link HttpURLConnection}, 9 | * used as the default client when a custom one is not specified by the application. 10 | * 11 | * @author fabiocarvalho777@gmail.com 12 | */ 13 | final public class MoccaDefaultHttpClient extends MoccaHttpClient { 14 | 15 | /** 16 | * Creates a new Mocca default HTTP client using 17 | * default configuration 18 | */ 19 | public MoccaDefaultHttpClient() { 20 | this(null, null); 21 | } 22 | 23 | /** 24 | * Creates a new Mocca default HTTP client using 25 | * user defined configuration 26 | * 27 | * @param sslContextFactory the SSL context factory to be used with this client 28 | * @param hostnameVerifier the hostname verifier to be used with this client 29 | */ 30 | public MoccaDefaultHttpClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) { 31 | super(new feign.Client.Default(sslContextFactory, hostnameVerifier)); 32 | } 33 | 34 | /** 35 | * Creates a new Mocca default HTTP client using 36 | * user defined configuration 37 | * 38 | * @param sslContextFactory the SSL context factory to be used with this client 39 | * @param hostnameVerifier the hostname verifier to be used with this client 40 | * @param disableRequestBuffering whether to disable or not request buffering 41 | */ 42 | public MoccaDefaultHttpClient(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier, boolean disableRequestBuffering) { 43 | super(new feign.Client.Default(sslContextFactory, hostnameVerifier, disableRequestBuffering)); 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /.github/workflows/gradle-publish.yml: -------------------------------------------------------------------------------- 1 | name: Release Publishing 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | 17 | - name: Set up JDK 18 | uses: actions/setup-java@v3 19 | with: 20 | java-version: '11' 21 | distribution: 'temurin' 22 | server-id: github # Value of the distributionManagement/repository/id field of the pom.xml 23 | settings-path: ${{ github.workspace }} # location for the settings.xml file 24 | 25 | - name: Build with Gradle 26 | uses: gradle/gradle-build-action@v2.4.2 27 | with: 28 | arguments: build 29 | 30 | # Secrets are managed by GitHub (see https://docs.github.com/en/actions/security-guides/encrypted-secrets) 31 | - name: Retrieve, decode, import and export secring 32 | env: 33 | SECRING_BASE64: ${{ secrets.SECRING_BASE64 }} 34 | run: | 35 | echo $SECRING_BASE64 | base64 --decode > secring.gpg 36 | gpg --batch --import secring.gpg 37 | 38 | # The USERNAME and TOKEN need to correspond to the credentials environment variables used in 39 | # the publishing section of your build.gradle 40 | # Secrets are managed by GitHub (see https://docs.github.com/en/actions/security-guides/encrypted-secrets) 41 | - name: Publish to Sonatype Nexus 42 | uses: gradle/gradle-build-action@v2.4.2 43 | with: 44 | arguments: publish 45 | env: 46 | SONATYPE_USER: ${{ secrets.SONATYPE_USER }} 47 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 48 | GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }} 49 | GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} 50 | SECRING_FILE: ${{ github.workspace }}/secring.gpg -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaExecutorHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.AsyncClient; 4 | 5 | import java.util.concurrent.ExecutorService; 6 | 7 | /** 8 | * A Mocca async HTTP client based on a Mocca sync HTTP client 9 | * whose executions are delegated to, and run by, 10 | * an user provided {@link ExecutorService}. 11 | *
12 | * An example of how to use it can be seen below. 13 | * In this example, {@code AsyncBooksAppClient} is the application defined client API, 14 | * where all GraphQL operation methods return {@link java.util.concurrent.CompletableFuture}. 15 | * 16 | *
17 |  * {@code
18 |  *         ExecutorService executorService = Executors.newCachedThreadPool();
19 |  *         MoccaExecutorHttpClient executorClient = new MoccaExecutorHttpClient<>(new MoccaOkHttpClient(), executorService);
20 |  *
21 |  *         AsyncBooksAppClient client = MoccaClient.Builder
22 |  *                 .async(getBaseUri().toString())
23 |  *                 .client(executorClient)
24 |  *                 .build(AsyncBooksAppClient.class);
25 |  * }
26 |  * 
27 | * 28 | * @param the HTTP client type 29 | * @author fabiocarvalho777@gmail.com 30 | */ 31 | final public class MoccaExecutorHttpClient extends MoccaAsyncHttpClient { 32 | 33 | /** 34 | * A Mocca async HTTP client based on a Mocca sync HTTP client 35 | * whose executions are delegated to, and run by, 36 | * an user provided {@link ExecutorService} 37 | * 38 | * @param moccaHttpClient the Mocca sync HTTP client to perform the GraphQL remote calls 39 | * @param executorService the executor service in charge of running all GraphQL remote calls 40 | */ 41 | public MoccaExecutorHttpClient(MoccaHttpClient moccaHttpClient, ExecutorService executorService) { 42 | super(new AsyncClient.Default<>(moccaHttpClient.getFeignClient(), executorService)); 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /mocca-micrometer/src/main/java/com/paypal/mocca/client/MoccaMicrometerCapability.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.micrometer.MicrometerCapability; 4 | import io.micrometer.core.instrument.Meter; 5 | import io.micrometer.core.instrument.MeterRegistry; 6 | import io.micrometer.core.instrument.config.MeterFilter; 7 | 8 | /** 9 | * Mocca supports Micrometer-based metrics, which primarily revolve around HTTP interactions with the target GraphQL server. 10 | * Notice Mocca metrics are identified with {@code mocca.} prefix. 11 | *
12 | * The example below shows how to enable metric gathering in Mocca using Micrometer: 13 | *

14 |  * import io.micrometer.core.instrument.simple.SimpleMeterRegistry;
15 |  *
16 |  * ...
17 |  *
18 |  * SimpleMeterRegistry meterRegistry = new SimpleMeterRegistry();
19 |  *
20 |  * BooksAppClient micrometerEnabledClient = MoccaClient.Builder
21 |  *     .sync("localhost:8080/booksapp")
22 |  *     .addCapability(new MoccaMicrometerCapability(meterRegistry))
23 |  *     .build(BooksAppClient.class);
24 |  * 
25 | * 26 | * @author crankydillo@gmail.com 27 | */ 28 | public final class MoccaMicrometerCapability extends MoccaCapability { 29 | 30 | /** 31 | * Creates a new {@link MoccaMicrometerCapability} 32 | * 33 | * @param meterRegistry the meter registry to be registered 34 | */ 35 | public MoccaMicrometerCapability(final MeterRegistry meterRegistry) { 36 | super(new MicrometerCapability(moccafy(meterRegistry))); 37 | } 38 | 39 | /** 40 | * This mutates the argument! 41 | */ 42 | private static MeterRegistry moccafy(final MeterRegistry meterRegistry) { 43 | meterRegistry.config().meterFilter(new MeterFilter() { 44 | @Override 45 | public Meter.Id map(Meter.Id id) { 46 | return id.withName(id.getName().replaceAll("feign", "mocca")); 47 | } 48 | }); 49 | return meterRegistry; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/repository/BookRepository.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.repository; 2 | 3 | import com.paypal.mocca.model.Book; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Book repository which caches books data 12 | */ 13 | public class BookRepository { 14 | /** 15 | * Books store 16 | */ 17 | private static final Map BOOK_MAP = new HashMap<>(); 18 | 19 | /** 20 | * Constructor which loads initial data 21 | */ 22 | public BookRepository() { 23 | Book book1 = new Book(); 24 | book1.setId(1); 25 | book1.setName("Book1"); 26 | 27 | Book book2 = new Book(); 28 | book2.setId(2); 29 | book2.setName("Book2"); 30 | BOOK_MAP.put(book1.getId(), book1); 31 | BOOK_MAP.put(book2.getId(), book2); 32 | } 33 | 34 | /** 35 | * Save book to store. Calculates id based on map size 36 | * 37 | * @param name book name 38 | * @param authorId book author id 39 | * @return Saved book with id updated 40 | */ 41 | public Book save(String name, int authorId) { 42 | final int id = BOOK_MAP.size() + 1; 43 | Book book = new Book(); 44 | book.setId(id); 45 | book.setName(name); 46 | book.setAuthorId(authorId); 47 | BOOK_MAP.put(id, book); 48 | return book; 49 | } 50 | 51 | /** 52 | * Get book by id. Null if book does not exists. 53 | * 54 | * @param id book id 55 | * @return Book 56 | */ 57 | public Book get(int id) { 58 | return BOOK_MAP.get(id); 59 | } 60 | 61 | /** 62 | * Get all books 63 | * 64 | * @return all saved books 65 | */ 66 | public List getAll() { 67 | return BOOK_MAP.entrySet() 68 | .stream().map(Map.Entry::getValue) 69 | .collect(Collectors.toList()); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.FeignException; 4 | 5 | import javax.validation.ConstraintViolationException; 6 | import java.util.HashSet; 7 | import java.util.Set; 8 | 9 | /** 10 | * This exception handler allows Mocca to hide Feign exception types from applications 11 | * 12 | * @author facarvalho 13 | */ 14 | final class MoccaExceptionHandler { 15 | 16 | private MoccaExceptionHandler() { 17 | } 18 | 19 | private static final Set> acceptableExceptions = new HashSet<>(); 20 | 21 | static { 22 | // Only runtime exceptions can be added here 23 | acceptableExceptions.add(MoccaException.class); 24 | acceptableExceptions.add(ConstraintViolationException.class); 25 | } 26 | 27 | /** 28 | * If an {@link Error} is provided as parameter, that will be returned. 29 | * If a checked exception is provided as parameter, that will be returned. 30 | * If a {@link FeignException} is provided as parameter containing as cause 31 | * one of the types in {@code acceptableExceptions}, then the Feign exception 32 | * will be discarded, and its cause will be returned. 33 | * If any other {@link RuntimeException} is provided as parameter, that will be returned 34 | * wrapped in a {@link MoccaException} 35 | * 36 | * @param throwable a throwable to handled by Mocca 37 | * @return a throwable that maybe the same given as parameter or not 38 | */ 39 | static Throwable handleException(Throwable throwable) { 40 | if (throwable instanceof FeignException) { 41 | Throwable cause = throwable.getCause(); 42 | if (cause != null && acceptableExceptions.contains(cause.getClass())) { 43 | return cause; 44 | } 45 | } 46 | if (throwable instanceof RuntimeException) { 47 | return new MoccaException("The invocation of a client method has resulted in an exception: ", throwable); 48 | } 49 | return throwable; 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /release_steps.md: -------------------------------------------------------------------------------- 1 | # How to release Mocca 2 | 3 | ## Introduction 4 | 5 | This document explains how to release a new Mocca version. 6 | 7 | ## Pre-requirements 8 | 9 | 1. All instructions documented here are MacOS and bash specific. Adjust them accordingly if you use a different OS and/or shell. 10 | 1. You must have: 11 | 1. Admin rights to Mocca GitHub repo. 12 | 1. A [Nexus Repository Manager](https://oss.sonatype.org/#welcome) account with access to PayPal artifacts. 13 | 14 | # Release steps 15 | 16 | 1. Working from master branch 17 | 1. Run javadoc task to all non-test modules individually and make sure they all work 18 | 1. Set the new version in `build.gradle` 19 | 1. Run `./gradlew clean build` and make sure it succeeds 20 | 1. Set new version in end user document (`docs/END_USER_DOCUMENT.md` file) 21 | 1. Do a Search & Replace (there are many occurrences) 22 | 1. Add new version in `docs/RELEASE_NOTES.md` file 23 | 1. Commit `Setting version to x` 24 | 1. Push your changes (`git push upstream master`) 25 | 1. Go to Mocca repo in GitHub 26 | 1. Create a new release and tag from master branch 27 | 1. New release title and tag name should be the new version 28 | 1. Add sections `New Features and enhancements` and `Bug fixes` from release notes (in GitHub Markdown format) to Release description 29 | 1. Create the new release and make sure the GitHub action `Release Publishing` is automatically triggered and succeeds 30 | 1. Manual sonatype release 31 | 1. Go to [Nexus Repository Manager](https://oss.sonatype.org/#welcome) 32 | 1. Go to `Staging Repositories` 33 | 1. Make sure it has all modules and all of them have jars, javadoc and sources, all signed 34 | 1. Close the Mocca staging repository 35 | 1. Release the Mocca staging repository 36 | 1. Wait a couple of hours and make sure new artifacts version show at http://search.maven.org/#search|ga|1|g:com.paypal.mocca 37 | 1. Working from master branch 38 | 1. Set the new SNAPSHOT version in `build.gradle` 39 | 1. Run `./gradlew clean build` and make sure it succeeds 40 | 1. Commit `Setting version to ` 41 | 1. Push your changes (`git push upstream master`) -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contribution guidelines 3 | 4 | ## Reporting a bug or requesting a new feature 5 | 6 | Please, first search [Mocca issues](https://github.com/paypal/mocca/issues) and see if there is already an issue covering what you would like to report. If there is, feel free to add comments to it if you believe you have valuable information about it. 7 | 8 | If you couldn't find an issue covering what you would like to report, please feel free to open a new issue. If you are reporting a bug, please include the following information: 9 | 10 | 1. What you expected 11 | 1. What happened instead 12 | 1. Steps to recreate what happened 13 | 1. Error logs (if you have them) 14 | 15 | ## Contributing with code changes 16 | 17 | Follow the instructions below please: 18 | 19 | 1. First, please read the previous section and make sure there is an issue describing the bug fix or new feature you would like to provide. 20 | 1. Before starting writing code, please mention that in the issue comments section. The idea is to make sure there is no one else already working on it, or that that change is really expected for a following release. 21 | 1. Fork this repo 22 | 1. Checkout `develop` branch 23 | 1. Optionally, create your own feature branch on your fork out of `develop` branch 24 | 1. Apply your changes 25 | 1. Make sure all modules build and all unit tests pass 26 | 1. Make sure code coverage doesn't drop (add extra unit tests if necessary) 27 | 1. If fixing a bug, make sure you add an unit or functional test to expose the issue 28 | 1. If adding a new feature, make sure you add an unit or functional test to test the feature 29 | 1. If adding a new feature, add end user documentation as well 30 | 1. Add comments to the code explaining your changes if necessary 31 | 1. Create a pull request to the upstream `develop` branch (add the issue id in the end of PR and commit name preceded by a hashtag) 32 | 33 | ## Code style 34 | Make sure to follow the code style of the existing code. That means, for example, four spaces for indentation. 35 | 36 | ## More information 37 | Read more about best practices in [this github guide](https://guides.github.com/activities/contributing-to-open-source/). 38 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/test/java/com/paypal/mocca/functional/AbstractFunctionalTests.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.functional; 2 | 3 | import com.paypal.mocca.client.MoccaClient; 4 | import com.paypal.mocca.client.MoccaOkHttpClient; 5 | import com.paypal.mocca.server.GraphQLEndpoint; 6 | import okhttp3.OkHttpClient; 7 | import org.glassfish.jersey.jackson.JacksonFeature; 8 | import org.glassfish.jersey.server.ResourceConfig; 9 | import org.glassfish.jersey.test.JerseyTestNg; 10 | import org.glassfish.jersey.test.TestProperties; 11 | import org.glassfish.jersey.test.jetty.JettyTestContainerFactory; 12 | import org.glassfish.jersey.test.spi.TestContainerException; 13 | import org.glassfish.jersey.test.spi.TestContainerFactory; 14 | 15 | import javax.ws.rs.core.Application; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * Abstract functional tests which spins up Jersey container 20 | */ 21 | public abstract class AbstractFunctionalTests extends JerseyTestNg.ContainerPerClassTest { 22 | 23 | /** 24 | * Mocca GraphQL Client 25 | */ 26 | protected BooksAppClient client; 27 | 28 | AbstractFunctionalTests() { 29 | okhttp3.OkHttpClient okHttpClient = new OkHttpClient().newBuilder() 30 | .readTimeout(1, TimeUnit.SECONDS) 31 | .build(); 32 | 33 | client = MoccaClient.Builder 34 | .sync(getBaseUri().toString()) 35 | .client(new MoccaOkHttpClient(okHttpClient)) 36 | .build(BooksAppClient.class); 37 | } 38 | 39 | /** 40 | * Configures Jersey 41 | * 42 | * @return JAX-RS application 43 | */ 44 | @Override 45 | protected Application configure() { 46 | enable(TestProperties.LOG_TRAFFIC); 47 | enable(TestProperties.DUMP_ENTITY); 48 | ResourceConfig resourceConfig = new ResourceConfig(); 49 | resourceConfig.register(JacksonFeature.class); 50 | resourceConfig.register(GraphQLEndpoint.class); 51 | return resourceConfig; 52 | } 53 | 54 | /** 55 | * Test container factory 56 | * 57 | * @return TestContainerFactory 58 | * @throws TestContainerException if Netty cannot be instantiated 59 | */ 60 | @Override 61 | protected TestContainerFactory getTestContainerFactory() throws TestContainerException { 62 | return new JettyTestContainerFactory(); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.paypal.mocca/mocca-client/badge.svg?style=flat)](http://search.maven.org/#search|ga|1|g:com.paypal.mocca) 2 | [![javadoc](https://javadoc.io/badge2/com.paypal.mocca/mocca-client/javadoc.svg)](https://javadoc.io/doc/com.paypal.mocca/mocca-client) 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | 5 |

6 |
7 |
8 | 9 | # Mocca 10 | 11 | Mocca is a GraphQL client for JVM languages with the goal of being easy to use, flexible and modular. With that in mind, Mocca was designed to offer: 12 | 13 | 1. Simple and intuitive API 14 | 1. Good end user documentation 15 | 1. Pluggable HTTP clients 16 | 1. Pluggable components, relying on great open source libraries, allowing features such as code generation, resilience, parsers and observability 17 | 18 | ## Features 19 | 20 | Mocca offers support for: 21 | 22 | 1. GraphQL features 23 | 1. GraphQL queries and mutations 24 | 1. Automatic variable definition 25 | 1. Automatic selection set definition based on DTO response type 26 | 1. Annotation and String based custom input variables 27 | 1. Annotation and String based custom selection set 28 | 1. Static and dynamic HTTP request headers 29 | 1. Observability via Micrometer 30 | 1. Resilience via Resilience4J 31 | 1. Flexible API allowing various pluggable HTTP clients 32 | 1. Asynchronous support 33 | 1. CompletableFuture 34 | 1. Pluggable asynchronous HTTP clients 35 | 1. User provided executor services 36 | 1. Request parameters validation via Bean Validation 37 | 38 | ## Quick start 39 | 40 | Please read the **Quick Start** section in [Mocca documentation](docs/END_USER_DOCUMENT.md) for instructions on how to start using Mocca quickly. 41 | 42 | ## End-user documentation 43 | 44 | Please refer to [Mocca documentation](docs/END_USER_DOCUMENT.md). 45 | 46 | ## Release notes 47 | See [Mocca release notes](docs/RELEASE_NOTES.md). 48 | 49 | ## Reporting an issue 50 | Please open an issue using our [GitHub issues](https://github.com/paypal/mocca/issues) page. 51 | 52 | ## Contributing 53 | You are very welcome to contribute to Mocca! Read our [Contribution guidelines](docs/CONTRIBUTING.md). 54 | 55 | ## License 56 | This project is licensed under the [MIT License](LICENSE.txt). 57 | -------------------------------------------------------------------------------- /mocca-functional-tests/build.gradle: -------------------------------------------------------------------------------- 1 | import io.github.kobylynskyi.graphql.codegen.gradle.GraphQLCodegenGradleTask 2 | 3 | plugins { 4 | id "io.github.kobylynskyi.graphql.codegen" version "4.1.5" 5 | } 6 | 7 | /** 8 | * Server side code generation 9 | */ 10 | compileJava.dependsOn "graphqlCodegen" 11 | sourceSets.main.java.srcDir "$buildDir/generated" 12 | graphqlCodegen { 13 | graphqlSchemas.rootDir = "$projectDir/src/main/resources/schema" 14 | outputDir = new File("$buildDir/generated") 15 | apiPackageName = "com.paypal.mocca.api" 16 | modelPackageName = "com.paypal.mocca.model" 17 | modelValidationAnnotation = "" 18 | parentInterfaces { 19 | queryResolver = "graphql.kickstart.tools.GraphQLQueryResolver" 20 | mutationResolver = "graphql.kickstart.tools.GraphQLMutationResolver" 21 | } 22 | generateApis = true 23 | apiInterfaceStrategy = "DO_NOT_GENERATE" 24 | addGeneratedAnnotation = false 25 | } 26 | 27 | /** 28 | * Generate only model for Mocca client 29 | */ 30 | compileJava.dependsOn "graphqlCodegenClient" 31 | sourceSets.main.java.srcDir "$buildDir/generated-client" 32 | task graphqlCodegenClient(type: GraphQLCodegenGradleTask) { 33 | graphqlSchemas.rootDir = "$projectDir/src/main/resources/schema" 34 | outputDir = new File("$buildDir/generated-client") 35 | modelPackageName = "com.paypal.mocca.client.model" 36 | generateApis = false 37 | generateClient = false 38 | generateParameterizedFieldsResolvers = false 39 | modelValidationAnnotation = "" 40 | addGeneratedAnnotation = false 41 | } 42 | 43 | dependencies { 44 | implementation lib.jersey_hk2, 45 | lib.graphql_tools, 46 | lib.slf4j_api, 47 | lib.javax_servlet, 48 | lib.jersey_media_json_jackson, 49 | lib.jackson_provider, 50 | lib.logback, 51 | lib.jetty_server, 52 | lib.validation_api 53 | 54 | runtimeOnly lib.hibernate_validator, 55 | lib.jakarta_el 56 | 57 | testImplementation project(':mocca-client'), 58 | project(':mocca-okhttp'), 59 | project(':mocca-hc5'), 60 | project(':mocca-micrometer'), 61 | project(':mocca-resilience4j'), 62 | lib.testng, 63 | lib.jersey_test, 64 | lib.jersey_jetty, 65 | lib.hibernate_validator 66 | } 67 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/MoccaExceptionHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.FeignException; 4 | import feign.codec.EncodeException; 5 | import org.testng.annotations.Test; 6 | 7 | import javax.validation.ConstraintViolationException; 8 | import java.util.Collections; 9 | 10 | import static org.testng.Assert.assertEquals; 11 | import static org.testng.Assert.assertTrue; 12 | 13 | public class MoccaExceptionHandlerTest { 14 | 15 | @Test 16 | public void errorTest() { 17 | Error error = new OutOfMemoryError("too much to remember"); 18 | assertEquals(MoccaExceptionHandler.handleException(error), error); 19 | } 20 | 21 | @Test 22 | public void constraintViolationExceptionTest() { 23 | ConstraintViolationException constraintViolationException = new ConstraintViolationException(Collections.emptySet()); 24 | FeignException feignException = new EncodeException("Encode exception", constraintViolationException); 25 | assertEquals(MoccaExceptionHandler.handleException(feignException), constraintViolationException); 26 | } 27 | 28 | @Test 29 | public void moccaExceptionTest() { 30 | MoccaException moccaException = new MoccaException("Mocca must be hot"); 31 | FeignException feignException = new EncodeException("encode exception", moccaException); 32 | assertEquals(MoccaExceptionHandler.handleException(feignException), moccaException); 33 | } 34 | 35 | @Test 36 | public void feignExceptionNoCauseTest() { 37 | FeignException feignException = new EncodeException("encode exception wihout mocca"); 38 | Throwable throwable = MoccaExceptionHandler.handleException(feignException); 39 | assertTrue(throwable instanceof MoccaException); 40 | assertEquals(throwable.getCause(), feignException); 41 | } 42 | 43 | @Test 44 | public void feignExceptionWithCauseTest() { 45 | FeignException feignException = new EncodeException("encode exception", new RuntimeException("foo")); 46 | Throwable throwable = MoccaExceptionHandler.handleException(feignException); 47 | assertTrue(throwable instanceof MoccaException); 48 | assertEquals(throwable.getCause(), feignException); 49 | } 50 | 51 | @Test 52 | public void nonFeignRuntimeExceptionNoCauseTest() { 53 | RuntimeException runtimeException = new RuntimeException("runtime exception"); 54 | Throwable throwable = MoccaExceptionHandler.handleException(runtimeException); 55 | assertTrue(throwable instanceof MoccaException); 56 | assertEquals(throwable.getCause(), runtimeException); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/MoccaClientMutationTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.sample.SampleClient; 4 | import com.paypal.mocca.client.sample.SampleRequestDTO; 5 | import com.paypal.mocca.client.sample.SampleResponseDTO; 6 | import org.testng.annotations.AfterClass; 7 | import org.testng.annotations.BeforeClass; 8 | import org.testng.annotations.Test; 9 | 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | import static org.testng.Assert.*; 14 | 15 | public class MoccaClientMutationTest { 16 | 17 | private SampleClient client; 18 | 19 | @BeforeClass 20 | private void setup() throws IOException { 21 | String serverBaseUrl = WireMockProvider.startServer(); 22 | client = MoccaClient.Builder.sync(serverBaseUrl).build(SampleClient.class); 23 | } 24 | 25 | @AfterClass 26 | public void teardown() { 27 | WireMockProvider.stopServer(); 28 | } 29 | 30 | @Test 31 | public void mutationTest() { 32 | SampleResponseDTO result = client.addSample("boo", "far"); 33 | assertNotNull(result); 34 | assertEquals(result.getFoo(), "boo"); 35 | assertEquals(result.getBar(), "far"); 36 | } 37 | 38 | @Test 39 | public void mutationVoidReturnTest() { 40 | SampleRequestDTO sampleRequestDTO = new SampleRequestDTO("moo", "czar 100%"); 41 | client.addSample(sampleRequestDTO); 42 | } 43 | 44 | @Test 45 | public void mutationNoDataTest() { 46 | SampleResponseDTO result = client.addSample("moo", "czar 100%"); 47 | assertNull(result); 48 | } 49 | 50 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "(Internal Server Error\\(s\\) while executing query)") 51 | public void mutationErrorTest() { 52 | client.addSample("zoo", "car"); 53 | } 54 | 55 | @Test 56 | public void mutationListTest() { 57 | List sampleReponseDTOS = client.addSampleReturnList("boo", "far"); 58 | assertNotNull(sampleReponseDTOS); 59 | assertEquals(sampleReponseDTOS.size(), 2); 60 | assertEquals(sampleReponseDTOS.get(0).getFoo(), "boo1"); 61 | assertEquals(sampleReponseDTOS.get(0).getBar(), "far1"); 62 | assertEquals(sampleReponseDTOS.get(1).getFoo(), "boo2"); 63 | assertEquals(sampleReponseDTOS.get(1).getBar(), "far2"); 64 | } 65 | 66 | @Test 67 | public void mutationListNoDataTest() { 68 | List sampleReponseDTOS = client.addSampleReturnList("moo", "czar"); 69 | assertNotNull(sampleReponseDTOS); 70 | assertEquals(sampleReponseDTOS.size(), 0); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /mocca-resilience4j/src/test/java/com/paypal/mocca/client/MoccaResilience4jTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import io.github.resilience4j.bulkhead.Bulkhead; 4 | import io.github.resilience4j.circuitbreaker.CircuitBreaker; 5 | import io.github.resilience4j.feign.FeignDecorators; 6 | import io.github.resilience4j.ratelimiter.RateLimiter; 7 | import io.github.resilience4j.retry.Retry; 8 | import org.mockito.Mockito; 9 | import org.testng.annotations.BeforeMethod; 10 | import org.testng.annotations.Test; 11 | 12 | import java.util.function.Function; 13 | 14 | import static org.mockito.Mockito.mock; 15 | import static org.mockito.Mockito.verify; 16 | 17 | public class MoccaResilience4jTest { 18 | final FeignDecorators.Builder decoratedBuilder= mock(FeignDecorators.Builder.class); 19 | 20 | @BeforeMethod 21 | void resetBuilderMock() { 22 | Mockito.reset(decoratedBuilder); 23 | } 24 | 25 | @Test 26 | void delegatedBuild() { 27 | new MoccaResilience4j.Builder(decoratedBuilder).build(); 28 | verify(decoratedBuilder).build(); 29 | } 30 | 31 | @Test 32 | void bulkheadTest() { 33 | final Bulkhead bulkhead = Bulkhead.ofDefaults("defaults"); 34 | new MoccaResilience4j.Builder(decoratedBuilder).bulkhead(bulkhead); 35 | verify(decoratedBuilder).withBulkhead(bulkhead); 36 | } 37 | 38 | @Test 39 | void retryTest() { 40 | final Retry retry = Retry.ofDefaults("defaults"); 41 | new MoccaResilience4j.Builder(decoratedBuilder).retry(retry); 42 | verify(decoratedBuilder).withRetry(retry); 43 | } 44 | 45 | @Test 46 | void circuitBreakerTest() { 47 | final CircuitBreaker cb = CircuitBreaker.ofDefaults("defaults"); 48 | new MoccaResilience4j.Builder(decoratedBuilder).circuitBreaker(cb); 49 | verify(decoratedBuilder).withCircuitBreaker(cb); 50 | } 51 | 52 | @Test 53 | void rateLimiterTest() { 54 | final RateLimiter rl = RateLimiter.ofDefaults("defaults"); 55 | new MoccaResilience4j.Builder(decoratedBuilder).rateLimiter(rl); 56 | verify(decoratedBuilder).withRateLimiter(rl); 57 | } 58 | 59 | @Test 60 | void fallBackTest() { 61 | final Object fallback = Mockito.any(); 62 | new MoccaResilience4j.Builder(decoratedBuilder).fallback(fallback); 63 | verify(decoratedBuilder).withFallback(fallback); 64 | } 65 | 66 | @Test 67 | void fallBackFactoryTest() { 68 | final Function fallbackFactory = Mockito.any(); 69 | new MoccaResilience4j.Builder(decoratedBuilder).fallbackFactory(fallbackFactory); 70 | verify(decoratedBuilder).withFallbackFactory(fallbackFactory); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/ComplexSampleType.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | public class ComplexSampleType { 4 | 5 | public static class ComplexField { 6 | int innerIntVar; 7 | String innerStringVar; 8 | boolean innerBooleanVar; 9 | 10 | public ComplexField(int innerIntVar, String innerStringVar, boolean innerBooleanVar) { 11 | this.innerIntVar = innerIntVar; 12 | this.innerStringVar = innerStringVar; 13 | this.innerBooleanVar = innerBooleanVar; 14 | } 15 | 16 | public int getInnerIntVar() { 17 | return innerIntVar; 18 | } 19 | 20 | public ComplexField setInnerIntVar(int innerIntVar) { 21 | this.innerIntVar = innerIntVar; 22 | return this; 23 | } 24 | 25 | public String getInnerStringVar() { 26 | return innerStringVar; 27 | } 28 | 29 | public ComplexField setInnerStringVar(String innerStringVar) { 30 | this.innerStringVar = innerStringVar; 31 | return this; 32 | } 33 | 34 | public boolean isInnerBooleanVar() { 35 | return innerBooleanVar; 36 | } 37 | 38 | public ComplexField setInnerBooleanVar(boolean innerBooleanVar) { 39 | this.innerBooleanVar = innerBooleanVar; 40 | return this; 41 | } 42 | } 43 | 44 | private int intVar; 45 | private String stringVar; 46 | private boolean booleanVar; 47 | private ComplexField complexField; 48 | 49 | public ComplexSampleType(int intVar, String stringVar, boolean booleanVar, ComplexField complexField) { 50 | this.intVar = intVar; 51 | this.stringVar = stringVar; 52 | this.booleanVar = booleanVar; 53 | this.complexField = complexField; 54 | } 55 | 56 | public int getIntVar() { 57 | return intVar; 58 | } 59 | 60 | public ComplexSampleType setIntVar(int intVar) { 61 | this.intVar = intVar; 62 | return this; 63 | } 64 | 65 | public String getStringVar() { 66 | return stringVar; 67 | } 68 | 69 | public ComplexSampleType setStringVar(String stringVar) { 70 | this.stringVar = stringVar; 71 | return this; 72 | } 73 | 74 | public boolean isBooleanVar() { 75 | return booleanVar; 76 | } 77 | 78 | public ComplexSampleType setBooleanVar(boolean booleanVar) { 79 | this.booleanVar = booleanVar; 80 | return this; 81 | } 82 | 83 | public ComplexField getComplexField() { 84 | return complexField; 85 | } 86 | 87 | public ComplexSampleType setComplexField(ComplexField complexField) { 88 | this.complexField = complexField; 89 | return this; 90 | } 91 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/RequestHeader.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | /** 9 | * Annotation used to add HTTP headers to a Mocca client. 10 | *
11 | *
12 | * HTTP headers can be set in Mocca at client API or method level using {@link RequestHeader} annotation. When {@link RequestHeader} annotation is added at client API level, the header will be included across all requests made through that client. The example below shows a header added at client API level. 13 | *

14 |  * @RequestHeader("sampleheader:samplevalue")
15 |  * public interface BooksAppClient extends MoccaClient {
16 |  *
17 |  *     ...
18 |  *
19 |  * }
20 |  * 
21 | * As seen in the example above, a header is set as a String, where the name is followed by a colon and the header value. 22 | *
23 | * The example below shows the usage of multiple headers at method level, added as an array of Strings. 24 | *

25 |  * public interface BooksAppClient extends MoccaClient {
26 |  *
27 |  *     @Query
28 |  *     @RequestHeader({"sampleheader:samplevalue", "anotherheader:anothervalue"})
29 |  *     Book getBookById(int bookId);
30 |  *
31 |  *     ...
32 |  *
33 |  * }
34 |  * 
35 | * All header values in above examples are defined statically. If the application needs to set header values dynamically, annotation {@link RequestHeaderParam} can be used with GraphQL operation method parameters, as seen in the example below. Notice the header value is identified using a placeholder surrounded by curly braces. 36 | *

37 |  * public interface BooksAppClient extends MoccaClient {
38 |  *
39 |  *     @Query
40 |  *     @RequestHeader("sampleheader: {headervalue}")
41 |  *     Book getBookById(@RequestHeaderParam("headervalue") String dynamicvalue, int bookId);
42 |  *
43 |  *     ...
44 |  *
45 |  * }
46 |  * 
47 | * A few important notes: 48 | *
    49 | *
  1. Dynamic headers can only be used at the method level
  2. 50 | *
  3. A method can have a mix of static and dynamic headers
  4. 51 | *
  5. An application can have as many client API and method level headers as necessary
  6. 52 | *
  7. If the same header is set at client API and method level, the one set at the method takes precedence
  8. 53 | *
  9. If the same header is defined at the same method multiple times, all specified values will be set at the header value using comma as separator
  10. 54 | *
55 | * 56 | * @see RequestHeaderParam 57 | * @author abprabhakar@paypal.com 58 | */ 59 | @Target({ElementType.METHOD, ElementType.TYPE}) 60 | @Retention(RetentionPolicy.RUNTIME) 61 | public @interface RequestHeader { 62 | 63 | /** 64 | * Array of headers in key:value format 65 | * 66 | * @return array of headers 67 | */ 68 | String[] value() default {}; 69 | 70 | } -------------------------------------------------------------------------------- /mocca-http-client-tests/src/main/java/com/paypal/mocca/client/BasicMoccaHttpClientTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.annotation.Query; 4 | import org.eclipse.jetty.server.Request; 5 | import org.eclipse.jetty.server.Server; 6 | import org.eclipse.jetty.server.handler.AbstractHandler; 7 | import org.testng.annotations.AfterClass; 8 | import org.testng.annotations.BeforeClass; 9 | import org.testng.annotations.Test; 10 | 11 | import javax.servlet.http.HttpServletRequest; 12 | import javax.servlet.http.HttpServletResponse; 13 | import java.io.IOException; 14 | 15 | import static org.testng.Assert.assertEquals; 16 | 17 | /** 18 | * Verifies that a supplied {@link MoccaHttpClient} works for 19 | * some basic GraphQL call. The general idea being that we're 20 | * just testing the basic HTTP parts function correctly (e.g. 21 | * POST requests). 22 | *

23 | * Unfortunately, there seems to be a Gradle or TestNG bug where 24 | * if a class contains no tests but extends one that does, then 25 | * Gradle build will not execute the test. Annotating the concrete 26 | * class with `@Test` appears to 'solve' the problem. 27 | */ 28 | abstract class BasicMoccaHttpClientTest { 29 | // TODO solve the gradlew problem described above. Some links: 30 | // https://discuss.gradle.org/t/testng-tests-that-inherit-from-a-base-class-but-do-not-add-new-test-methods-are-not-detected/1259 31 | // https://stackoverflow.com/questions/64087969/testng-cannot-find-test-methods-with-inheritance 32 | 33 | private static final String GRAPHQL_GREETING = "Hello!"; 34 | 35 | private final MoccaHttpClient moccaHttpClient; 36 | private Server graphqlServer; 37 | private SampleDataClient sampleDataClient; 38 | 39 | BasicMoccaHttpClientTest(MoccaHttpClient moccaHttpClient) { 40 | this.moccaHttpClient = Arguments.requireNonNull(moccaHttpClient); 41 | } 42 | 43 | @BeforeClass 44 | public void setUp() throws Exception { 45 | final int port = 0; // signals use random port 46 | graphqlServer = new Server(port); 47 | graphqlServer.setHandler(new AbstractHandler() { 48 | @Override 49 | public void handle(String target, Request baseRequest, HttpServletRequest req, HttpServletResponse resp) 50 | throws IOException { 51 | baseRequest.setHandled(true); 52 | resp.getWriter().write("{ \"data\": { \"greeting\": \"" + GRAPHQL_GREETING + "\" } }"); 53 | } 54 | }); 55 | graphqlServer.start(); 56 | sampleDataClient = MoccaClient.Builder.sync(graphqlServer.getURI().toASCIIString()) 57 | .client(moccaHttpClient) 58 | .build(SampleDataClient.class); 59 | } 60 | 61 | @AfterClass 62 | public void tearDown() throws Exception { 63 | graphqlServer.stop(); 64 | } 65 | 66 | @Test(description = "Basic GraphQL call") 67 | void testBasic() { 68 | final String greeting = sampleDataClient.greeting(); 69 | assertEquals(greeting, GRAPHQL_GREETING); 70 | } 71 | 72 | public interface SampleDataClient extends MoccaClient { 73 | @Query 74 | String greeting(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/MoccaDeserializerTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.sample.SampleResponseDTO; 4 | import org.testng.annotations.Test; 5 | 6 | import java.io.ByteArrayInputStream; 7 | 8 | /** 9 | * Unit tests for {@link MoccaDeserializer} 10 | * 11 | * @author fabiocarvalho777@gmail.com 12 | */ 13 | public class MoccaDeserializerTest { 14 | 15 | private final MoccaDeserializer moccaDeserializer = new MoccaDeserializer(); 16 | 17 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "Response does not include data nor errors JSON fields") 18 | public void invalidResponse1Test() { 19 | String response = "{}"; 20 | ByteArrayInputStream inputStream = new ByteArrayInputStream(response.getBytes()); 21 | moccaDeserializer.deserialize(inputStream, SampleResponseDTO.class, "getOneSample"); 22 | } 23 | 24 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "Response contains an empty errors list") 25 | public void invalidResponse2Test() { 26 | String response = "{\"errors\": [],\"data\": {\"getOneSample\": null}}"; 27 | ByteArrayInputStream inputStream = new ByteArrayInputStream(response.getBytes()); 28 | moccaDeserializer.deserialize(inputStream, SampleResponseDTO.class, "getOneSample"); 29 | } 30 | 31 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "Response contains a null errors field") 32 | public void invalidResponse3Test() { 33 | String response = "{\"errors\": null,\"data\": {\"getOneSample\": null}}"; 34 | ByteArrayInputStream inputStream = new ByteArrayInputStream(response.getBytes()); 35 | moccaDeserializer.deserialize(inputStream, SampleResponseDTO.class, "getOneSample"); 36 | } 37 | 38 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "\\[\\{\"message\":\"error 1\"},\\{\"message\":\"error 2\"}]") 39 | public void invalidResponse4Test() { 40 | String response = "{\"errors\": [{\"message\": \"error 1\"}, {\"message\": \"error 2\"}],\"data\": {\"getOneSample\": null}}"; 41 | ByteArrayInputStream inputStream = new ByteArrayInputStream(response.getBytes()); 42 | moccaDeserializer.deserialize(inputStream, SampleResponseDTO.class, "getOneSample"); 43 | } 44 | 45 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "Response JSON payload does not contain data for the requested operation: getOneSample") 46 | public void invalidResponse5Test() { 47 | String response = "{\"data\": {\"blah\": null}}"; 48 | ByteArrayInputStream inputStream = new ByteArrayInputStream(response.getBytes()); 49 | moccaDeserializer.deserialize(inputStream, SampleResponseDTO.class, "getOneSample"); 50 | } 51 | 52 | @Test(expectedExceptions = MoccaException.class, expectedExceptionsMessageRegExp = "Error processing response JSON payload") 53 | public void invalidResponse6Test() { 54 | String response = "{\"data\": }"; 55 | ByteArrayInputStream inputStream = new ByteArrayInputStream(response.getBytes()); 56 | moccaDeserializer.deserialize(inputStream, SampleResponseDTO.class, "getOneSample"); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem http://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/server/GraphQLFactory.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.server; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; 5 | import com.paypal.mocca.model.Author; 6 | import com.paypal.mocca.repository.AuthorRepository; 7 | import com.paypal.mocca.repository.BookRepository; 8 | import com.paypal.mocca.resolvers.MutationResolver; 9 | import com.paypal.mocca.resolvers.QueryResolver; 10 | import graphql.GraphQL; 11 | import graphql.kickstart.tools.GraphQLResolver; 12 | import graphql.kickstart.tools.PerFieldObjectMapperProvider; 13 | import graphql.kickstart.tools.SchemaParser; 14 | import graphql.kickstart.tools.SchemaParserBuilder; 15 | import graphql.kickstart.tools.SchemaParserOptions; 16 | import graphql.schema.GraphQLSchema; 17 | 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | /** 22 | * Factory which instantiates GraphQL 23 | */ 24 | public final class GraphQLFactory { 25 | /** 26 | * GraphQL instance 27 | */ 28 | private static GraphQL graphQL; 29 | 30 | /** 31 | * Static factory method 32 | * 33 | * @param objectMapper jackson objectmapper 34 | * @return GraphQL 35 | */ 36 | public static GraphQL getInstance(ObjectMapper objectMapper) { 37 | if (graphQL == null) { 38 | graphQL = graphQL(objectMapper); 39 | } 40 | return graphQL; 41 | } 42 | 43 | /** 44 | * GraphQL Schema representation. 45 | * 46 | * @param schemaParser Schema parser 47 | * @return GraphQLSchema 48 | */ 49 | public static GraphQLSchema graphQLSchema(SchemaParser schemaParser) { 50 | return schemaParser.makeExecutableSchema(); 51 | } 52 | 53 | /** 54 | * Instantiate new graphql instance 55 | * 56 | * @param objectMapper Jackson Objectmapper 57 | * @return GraphQL 58 | */ 59 | public static GraphQL graphQL(final ObjectMapper objectMapper) { 60 | return GraphQL.newGraphQL(graphQLSchema(schemaParser(objectMapper))).build(); 61 | } 62 | 63 | /** 64 | * Create new schema parser for graphql-java 65 | * 66 | * @param objectMapper Jackson Object mapper 67 | * @return SchemaParser 68 | */ 69 | public static SchemaParser schemaParser(final ObjectMapper objectMapper) { 70 | SchemaParserOptions.Builder optionsBuilder = SchemaParserOptions.newOptions(); 71 | optionsBuilder.introspectionEnabled(true); 72 | 73 | SchemaParserBuilder builder = new SchemaParserBuilder(); 74 | builder.files("schema/query.graphqls", 75 | "schema/book.graphqls", 76 | "schema/author.graphqls", 77 | "schema/mutation.graphqls"); 78 | builder.dictionary(Author.class); 79 | 80 | 81 | objectMapper.registerModule(new Jdk8Module()); 82 | PerFieldObjectMapperProvider perFieldObjectMapperProvider = fieldDefinition -> objectMapper; 83 | optionsBuilder.objectMapperProvider(perFieldObjectMapperProvider); 84 | final BookRepository bookRepository = new BookRepository(); 85 | final AuthorRepository authorRepository = new AuthorRepository(); 86 | List> resolvers = Arrays.asList( 87 | new QueryResolver(bookRepository, authorRepository), 88 | new MutationResolver(bookRepository, authorRepository) 89 | ); 90 | return builder.resolvers(resolvers).build(); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/SelectionSet.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | /** 10 | * Annotation used to define a GraphQL selection set, 11 | * to be used in combination with an operation annotation 12 | * in a method in a client API. 13 | *
14 | * When {@code SelectionSet} annotation is present, although Mocca won't resolve automatically the selection set using the return type, still the application has to make sure all fields in the provided custom selection set exist in the DTO used in the return type. 15 | *
16 | * {@code SelectionSet} would then behave like this: 17 | *
    18 | *
  1. If annotation is present and its value attribute is set, Mocca automatic selection set resolution is turned off, and {@code SelectionSet} value is used to define the selection set. In this case if ignore value is also set, then that is not used by Mocca, and a warning is logged.
  2. 19 | *
  3. If annotation is present, its value attribute is NOT set, but ignore is, then Mocca automatic selection set resolution is turned ON, and {@code SelectionSet} ignore is used to pick which response DTO fields to ignore from the selection set.
  4. 20 | *
  5. If annotation is present and both value and ignore attributes are NOT set, then a {@link com.paypal.mocca.client.MoccaException} is thrown.
  6. 21 | *
22 | * 23 | * See a client API example below. Notice the given selection set must be wrapped around curly braces. 24 | *

25 |  * import com.paypal.mocca.client.MoccaClient;
26 |  * import com.paypal.mocca.client.annotation.Mutation;
27 |  * import com.paypal.mocca.client.annotation.Query;
28 |  * import com.paypal.mocca.client.annotation.SelectionSet;
29 |  *
30 |  * public interface BooksAppClient extends MoccaClient {
31 |  *
32 |  *     @Query
33 |  *     @SelectionSet("{id, name}")
34 |  *     List<Book> getBooks(String variables);
35 |  *
36 |  *     @Query
37 |  *     @SelectionSet(ignore="title")
38 |  *     List<Book> getBookById(int bookId);
39 |  *
40 |  *     @Query
41 |  *     Book getBook(long id);
42 |  *
43 |  *     @Mutation
44 |  *     Author addAuthor(@Variable(ignore = "books")Author author);
45 |  *
46 |  *     @Mutation
47 |  *     Book addBook(Book book);
48 |  *
49 |  * }
50 | * 51 | * @author fabiocarvalho777@gmail.com 52 | */ 53 | @Retention(RUNTIME) 54 | @Target(ElementType.METHOD) 55 | public @interface SelectionSet { 56 | 57 | String UNDEFINED = "UNDEFINED"; 58 | 59 | /** 60 | * The actual GraphQL selection set in plain text. 61 | * The given selection set must be wrapped around curly braces. 62 | * 63 | * @return the user provided selection set 64 | */ 65 | String value() default UNDEFINED; 66 | 67 | /** 68 | * When the Response type is a POJO (following Java bean conventions), sometimes it is populated with certain properties 69 | * the application would like to be ignored by Mocca when generating the GraphQL Selection Set. 70 | * In order to do so, specify here an array containing all fields to be ignored in the Selection Set. 71 | * The name of a property in an inner POJO can be specified using the outer field name followed by dot. 72 | * If the request type is not a POJO, or if {@link #value()} is set, then {@code ignore} will have no effect. 73 | * If the property set to be ignored doesn't exist, then it has no effect, the Selection Set is generated normally as 74 | * if that ignore value had not been set. 75 | * If both {@link #value()} and {@code ignore} attributes are set, then the {@code ignore} value is not used by Mocca, and a warning is logged. 76 | * 77 | * @return an array containing all fields to be ignored in the return type, in case it is a DTO 78 | */ 79 | String[] ignore() default UNDEFINED; 80 | 81 | } 82 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/annotation/Var.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.Target; 6 | 7 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 8 | 9 | /** 10 | * Annotation used to configure GraphQL request variables. 11 | *
12 | * This annotation must be used in every parameter present in the operation method. All parameters together represent the variables section declared in the GraphQL schema of same operation. 13 | * A parameter name is not relevant, as the GraphQL variable name is defined in this annotation, but its type must be one of the following: 14 | *
    15 | *
  1. Any primitive type, or primitive wrapper 16 | *
  2. java.lang.String 17 | *
  3. A POJO following Java beans conventions 18 | *
  4. A java.util.List whose type can be any of the types mentioned earlier (except primitives) 19 | *
20 | *
21 | * See the client API example below. 22 | *

23 |  * import com.paypal.mocca.client.MoccaClient;
24 |  * import com.paypal.mocca.client.annotation.Mutation;
25 |  * import com.paypal.mocca.client.annotation.Query;
26 |  * import com.paypal.mocca.client.annotation.SelectionSet;
27 |  * import com.paypal.mocca.client.annotation.Var;
28 |  *
29 |  * public interface BooksAppClient extends MoccaClient {
30 |  *
31 |  *     @Query
32 |  *     @SelectionSet("{id, name}")
33 |  *     List<Book> getBooks(@Var("authorId") long authorId);
34 |  *
35 |  *     @Query
36 |  *     Book getBook(@Var("id") long id);
37 |  *
38 |  *     @Mutation
39 |  *     Author addAuthor(@Var(value = "author", ignore = "books") Author author);
40 |  *
41 |  *     @Mutation
42 |  *     Book addBook(@Var("book") Book book);
43 |  *
44 |  * }
45 | * 46 | * @author fabiocarvalho777@gmail.com 47 | */ 48 | @Retention(RUNTIME) 49 | @Target(ElementType.PARAMETER) 50 | public @interface Var { 51 | 52 | /** 53 | * Used to provide the name to the GraphQL variable. 54 | * This property is mandatory and must not be blank, 55 | * except when {@link #raw()} is set to true. 56 | * 57 | * @return the name of the GraphQL variable. 58 | */ 59 | String value() default ""; 60 | 61 | /** 62 | * When the variable is a POJO (following Java bean conventions), sometimes it is populated with certain properties 63 | * the application would like to be ignored by Mocca when serializing the GraphQL variables. 64 | * In order to do so, specify here an array containing all fields to be ignored in the POJO object. 65 | * The name of a property in an inner POJO can be specified using the outer field name followed by dot. 66 | * If the request type is not a POJO, or if {@link #raw()} is set to true, then {@code ignore} will have no effect. 67 | * If the property set to be ignored doesn't exist, then it has no effect, the POJO is serialized normally as 68 | * if that ignore value had been set. 69 | * 70 | * @return an array containing all fields to be ignored in a POJO variable 71 | */ 72 | String[] ignore() default {}; 73 | 74 | /** 75 | * It might be useful in certain cases to specify the whole GraphQL variables section as a String, 76 | * containing all variables inside of it, following the GraphQL specification. 77 | * In cases like this, this flag has to be set to true, only one parameter should be present, 78 | * and its type must be String. There is no need to wrap it with parenthesis though, as seen in the example below. 79 | * This feature is set to false by default. 80 | *

81 |      *
82 |      *     @Query
83 |      *     @SelectionSet("{id, name}")
84 |      *     List<Book> getBooks(@Var("authorId") String query);
85 |      *
86 |      * 
87 | * 88 | * @return whether all GraphQL variables should be set using one String parameter 89 | */ 90 | boolean raw() default false; 91 | 92 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaReflection.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import java.lang.reflect.Array; 4 | import java.lang.reflect.GenericArrayType; 5 | import java.lang.reflect.ParameterizedType; 6 | import java.lang.reflect.Type; 7 | import java.lang.reflect.TypeVariable; 8 | import java.lang.reflect.WildcardType; 9 | import java.util.HashSet; 10 | import java.util.Optional; 11 | import java.util.Set; 12 | 13 | /** 14 | * This class provide reflection utility static methods 15 | * used across Mocca project 16 | * 17 | * @author fabiocarvalho777@gmail.com 18 | */ 19 | final class MoccaReflection { 20 | 21 | private MoccaReflection() { 22 | } 23 | 24 | /* 25 | * Returns an optional containing the Type parameterized inside the given type, 26 | * if the given type is parameterized and equals to the outer reference type. 27 | * If it is not, an empty optional is returned. 28 | */ 29 | static Optional getInnerType(final Type type, final Type outerTypeReference) { 30 | if (!isParameterizedType(type)) return Optional.empty(); 31 | 32 | ParameterizedType parameterizedType = (ParameterizedType) type; 33 | if (parameterizedType.getRawType().getTypeName().equals(outerTypeReference.getTypeName())) { 34 | return Optional.of(parameterizedType.getActualTypeArguments()[0]); 35 | } else { 36 | return Optional.empty(); 37 | } 38 | } 39 | 40 | /* 41 | * Returns the first parametrized type inside the given type. 42 | * If the given type is not parameterized an IllegalArgumentException is thrown. 43 | */ 44 | static Type getInnerType(final Type type) { 45 | Arguments.require(isParameterizedType(type), "Given type is not parameterized"); 46 | 47 | ParameterizedType parameterizedType = (ParameterizedType) type; 48 | return parameterizedType.getActualTypeArguments()[0]; 49 | } 50 | 51 | /* 52 | * Returns true if the given type is a parameterized type. 53 | */ 54 | static boolean isParameterizedType(Type type) { 55 | Arguments.requireNonNull(type, "Type cannot be null"); 56 | 57 | return type instanceof ParameterizedType; 58 | } 59 | 60 | /* 61 | * Returns true if the given type is a parameterized type 62 | * and its raw type is the same as one of the given raw types 63 | */ 64 | static boolean isParameterizedType(Type type, Type... rawTypes) { 65 | Arguments.requireNonNull(type, "Type cannot be null"); 66 | Arguments.requireNonNull(rawTypes, "Raw type cannot be null"); 67 | 68 | if(!(type instanceof ParameterizedType)) return false; 69 | final Type actualRawType = ((ParameterizedType) type).getRawType(); 70 | 71 | for (Type t : rawTypes) if (t == actualRawType) return true; 72 | return false; 73 | } 74 | 75 | /* 76 | * Returns a class erased of any parameters, if there is any 77 | */ 78 | static Class erase(Type type) { 79 | if (type instanceof Class) { 80 | return (Class) type; 81 | } else if (type instanceof ParameterizedType) { 82 | ParameterizedType parameterizedType = (ParameterizedType) type; 83 | return (Class) parameterizedType.getRawType(); 84 | } else { 85 | Type[] typeVariableBounds; 86 | if (type instanceof TypeVariable) { 87 | TypeVariable typeVariable = (TypeVariable) type; 88 | typeVariableBounds = typeVariable.getBounds(); 89 | return 0 < typeVariableBounds.length ? erase(typeVariableBounds[0]) : Object.class; 90 | } else if (type instanceof WildcardType) { 91 | WildcardType wildcardType = (WildcardType) type; 92 | typeVariableBounds = wildcardType.getUpperBounds(); 93 | return 0 < typeVariableBounds.length ? erase(typeVariableBounds[0]) : Object.class; 94 | } else if (type instanceof GenericArrayType) { 95 | GenericArrayType genericArrayType = (GenericArrayType) type; 96 | return Array.newInstance(erase(genericArrayType.getGenericComponentType()), 0).getClass(); 97 | } else { 98 | throw new IllegalArgumentException("Unknown Type kind: " + type.getClass()); 99 | } 100 | } 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/MoccaClientBuilderTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.sample.AsyncSampleClient; 4 | import com.paypal.mocca.client.sample.SampleClient; 5 | import feign.Feign; 6 | import org.testng.annotations.Test; 7 | 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.util.List; 10 | import java.util.concurrent.AbstractExecutorService; 11 | import java.util.concurrent.ExecutionException; 12 | import java.util.concurrent.TimeUnit; 13 | 14 | import static org.testng.Assert.assertEquals; 15 | import static org.testng.Assert.assertTrue; 16 | import static org.testng.Assert.fail; 17 | 18 | public class MoccaClientBuilderTest { 19 | 20 | @Test 21 | void customExecutorServiceTest() throws Exception { 22 | class BadExecService extends AbstractExecutorService { 23 | private boolean wasUsed = false; 24 | 25 | public boolean wasUsed() { 26 | return wasUsed; 27 | } 28 | 29 | @Override 30 | public void execute(Runnable command) { 31 | wasUsed = true; 32 | command.run(); 33 | } 34 | 35 | @Override 36 | public void shutdown() { 37 | } 38 | 39 | @Override 40 | public List shutdownNow() { 41 | return null; 42 | } 43 | 44 | @Override 45 | public boolean isShutdown() { 46 | return false; 47 | } 48 | 49 | @Override 50 | public boolean isTerminated() { 51 | return false; 52 | } 53 | 54 | @Override 55 | public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { 56 | return false; 57 | } 58 | } 59 | 60 | final BadExecService badExecService = new BadExecService(); 61 | final MoccaHttpClient moccaJavaHttpClient = new MoccaDefaultHttpClient(); 62 | final MoccaExecutorHttpClient executorHttpClient = new MoccaExecutorHttpClient(moccaJavaHttpClient, badExecService); 63 | 64 | try { 65 | MoccaClient.Builder.async("http://localhost:8080") 66 | .client(executorHttpClient) 67 | .build(AsyncSampleClient.class) 68 | .getOneSample("boo" ,"far") 69 | .get(); 70 | } catch (final ExecutionException ee) { 71 | assertTrue(badExecService.wasUsed()); 72 | } 73 | } 74 | 75 | @Test 76 | void resiliency() throws Exception { 77 | // Resiliency is added by passing in a custom feign builder. 78 | // This test will just verify that is leveraged. 79 | final UnsupportedOperationException err = new UnsupportedOperationException("I'm not working today!"); 80 | class BadBuilder extends Feign.Builder { 81 | @Override 82 | public Feign build() { 83 | throw err; 84 | } 85 | } 86 | 87 | class BadResilience extends MoccaResiliency { 88 | public BadResilience() { 89 | super(new BadBuilder()); 90 | } 91 | } 92 | 93 | try { 94 | MoccaClient.Builder.sync("http://localhost:8080") 95 | .resiliency(new BadResilience()) 96 | .build(SampleClient.class); 97 | fail("Expected an exception to be the thrown."); 98 | } catch (final UnsupportedOperationException e) { 99 | assertEquals(e, err, "An expected error indicates custom `Feign.Builder` used."); 100 | } 101 | } 102 | 103 | @Test 104 | public void capabilitiesRegistration() { 105 | class MyCap extends MoccaCapability { 106 | public MyCap() { 107 | super(new PoorFeignCapability()); 108 | } 109 | } 110 | try { 111 | MoccaClient.Builder.sync("http://foo") 112 | .addCapability(new MyCap()) 113 | .build(SampleClient.class); 114 | fail("Expected an exception caused by MyCap."); 115 | } catch (final Exception e) { 116 | assertEquals( 117 | ((InvocationTargetException)e.getCause()).getTargetException().getMessage(), 118 | PoorFeignCapability.errorMessage 119 | ); 120 | } 121 | } 122 | } 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /docs/RELEASE_NOTES.md: -------------------------------------------------------------------------------- 1 | 2 | # Release notes 3 | 4 | ## 0.0.8 5 | 6 | #### Release date 7 | Nov 28th, 2023 8 | 9 | #### New features and enhancements 10 | 1. [Add support for enums as part of request and response parameters and types](https://github.com/paypal/mocca/issues/73) 11 | 1. [Log improvement](https://github.com/paypal/mocca/commit/0f15adb7edb65862c8884142d6b20504d03ac843) 12 | 13 | #### Bug fixes 14 | 1. [Jackson upgrade to version 2.14.2 to address CVEs](https://github.com/paypal/mocca/commit/dcb670a99e0fcf1aee6adee3435368793fed5c46) 15 | 16 | ## 0.0.7 17 | 18 | #### Release date 19 | May 4th, 2023 20 | 21 | #### New features and enhancements 22 | 1. [Add support for Duration and UUID as part of request and response parameters and types](https://github.com/paypal/mocca/issues/68) 23 | 24 | #### Bug fixes 25 | None 26 | 27 | ## 0.0.6 28 | 29 | #### Release date 30 | January 28th, 2022 31 | 32 | #### New features and enhancements 33 | None 34 | 35 | #### Bug fixes 36 | 1. [Fix classpath issue caused by conflicting versions of OkHttp](https://github.com/paypal/mocca/issues/61) 37 | 38 | ## 0.0.5 39 | 40 | #### Release date 41 | January 12th, 2022 42 | 43 | #### New features and enhancements 44 | None 45 | 46 | #### Bug fixes 47 | 1. [Dependency jackson-modules-java8 should be marked as pom](https://github.com/paypal/mocca/issues/59) 48 | 49 | ## 0.0.4 50 | 51 | #### Release date 52 | January 8th, 2021 53 | 54 | #### New features and enhancements 55 | 1. [Add support for Set data type inside POJOs used in the SelectionSet](https://github.com/paypal/mocca/issues/55) 56 | 1. [Upgrade key libraries to their latest available versions](https://github.com/paypal/mocca/issues/56) 57 | 58 | #### Bug fixes 59 | 1. [Mocca should unwrap user facing exceptions from Feign exceptions and hide Feign exceptions](https://github.com/paypal/mocca/issues/40) 60 | 1. [Fix typo in mocca-resilience4j module name](https://github.com/paypal/mocca/issues/54) 61 | 62 | ## 0.0.3 63 | 64 | #### Release date 65 | September 21st, 2021 66 | 67 | #### New features and enhancements 68 | 1. [Various mocca-client impls should get functionally tested](https://github.com/paypal/mocca/issues/32) 69 | 1. [Additional enhancements to support GraphQL variables and selection set data types corner cases](https://github.com/paypal/mocca/issues/30) 70 | 1. [Add support for Bean Validations annotations to validate GraphQL operation method parameters](https://github.com/paypal/mocca/issues/28) 71 | 1. [Enhance SelectionSet annotation to ignore fields in the return type](https://github.com/paypal/mocca/issues/27) 72 | 1. [Add support for operations with no variables by setting no parameters in the request method](https://github.com/paypal/mocca/issues/14) 73 | 1. [Mocca should support the usage of multiple method parameters as GraphQL variables](https://github.com/paypal/mocca/issues/13) 74 | 1. [Add support for Resilience4j Fallback](https://github.com/paypal/mocca/issues/11) 75 | 1. [JAXRS-based JagaHttpClient takes a JAXRS ClientBuilder, not Client.. ](https://github.com/paypal/mocca/issues/7) 76 | 1. [Hosted javadoc (UI)](https://github.com/paypal/mocca/issues/3) 77 | 78 | #### Bug fixes 79 | 1. [Fix serialization and deserialization issues and make sure client honors documentation](https://github.com/paypal/mocca/issues/24) 80 | 1. [Implement a mechanism in Mocca serializer to avoid cycles in request and response](https://github.com/paypal/mocca/issues/15) 81 | 82 | ## 0.0.2 83 | 84 | #### Release date 85 | June 29th, 2021 86 | 87 | #### New features and enhancements 88 | 1. Documentation enhancements 89 | 90 | #### Bug fixes 91 | 1. Sonatype artifacts publishing bug fix 92 | 93 | ## 0.0.1 94 | 95 | #### Release date 96 | June 21st, 2021 97 | 98 | #### New features and enhancements 99 | 1. GraphQL features 100 | 1. GraphQL queries and mutations via annotations `@Query` and `@Mutation` 101 | 1. Automatic variable definition based on DTO request type 102 | 1. Automatic selection set definition based on DTO response type 103 | 1. Annotation and String based custom input variables via annotation `@Variable` 104 | 1. Annotation and String based custom selection set via annotation `@SelectionSet` 105 | 1. Static and dynamic HTTP request headers support via annotations `@RequestHeader` and `@RequestHeaderParam` 106 | 1. Observability via Micrometer 107 | 1. Resilience with via Resilience4J 108 | 1. Flexible API allowing the following pluggable HTTP clients 109 | 1. OkHttp 110 | 1. Apache HTTP client 5 111 | 1. Apache HTTP client 112 | 1. Google HTTP client 113 | 1. Java HTTP2 client 114 | 1. JAX-RS 2 115 | 1. Asynchronous support 116 | 1. CompletableFuture 117 | 1. Pluggable asynchronous HTTP clients 118 | 1. User provided executor services 119 | 120 | #### Bug fixes 121 | None -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/SampleClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | import com.paypal.mocca.client.MoccaClient; 4 | import com.paypal.mocca.client.SampleEnum; 5 | import com.paypal.mocca.client.annotation.*; 6 | 7 | import java.time.Duration; 8 | import java.time.OffsetDateTime; 9 | import javax.validation.Valid; 10 | import javax.validation.constraints.NotNull; 11 | import java.util.List; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | @RequestHeader("classheader: classvalue") 16 | public interface SampleClient extends MoccaClient { 17 | 18 | @Query 19 | SampleResponseDTO getOneSample(); 20 | 21 | @Query 22 | SampleResponseDTO getOneSample(@Var(raw = true) String variables); 23 | 24 | @Query 25 | SampleResponseDTO getOneSample(@Var("foo") String foo, @Var("bar") String bar); 26 | 27 | @Query 28 | SampleResponseDTO getOneSampleNotNull(@Var("foo") @NotNull String foo, @Var("bar") @NotNull String bar); 29 | 30 | @Query 31 | List getSamplesList(@Var("foo") String foo, @Var("bar") String bar); 32 | 33 | @Query 34 | List getSamplesList(@Var("sampleRequests") List sampleRequests, @Var("numbers") List numbers, @Var("string") String string, @Var("number") int number); 35 | 36 | @Query 37 | SampleResponseDTO getOneSample(@Var("sampleRequest") SampleRequestDTO sampleRequestDTO); 38 | 39 | @Query 40 | SampleResponseDTO getOneValidSample(@Var("sampleRequest") @Valid ValidatedRequestDTO validatedRequestDTO); 41 | 42 | @Query 43 | SampleResponseDTO getOneSampleWithIgnore(@Var(value = "sampleRequest", ignore = "foo") SampleRequestDTO sampleRequestDTO); 44 | 45 | @Query 46 | @RequestHeader("sampleheader : { dynamicvalue }") 47 | SampleResponseDTO getOneSampleWithHeaderAndDTO(@RequestHeaderParam("dynamicvalue") String headerValue, @Var("sampleRequest") SampleRequestDTO sampleRequestDTO); 48 | 49 | @Query 50 | @RequestHeader("single : header") 51 | SampleResponseDTO getOneSampleWithSingleHeader(@Var("foo") String foo, @Var("bar") String bar); 52 | 53 | @Query 54 | @RequestHeader("classheader : anothervalue") 55 | SampleResponseDTO getOneSampleWithOverrideClassHeader(@Var("foo") String foo, @Var("bar") String bar); 56 | 57 | @Query 58 | @RequestHeader({"sampleheader: samplevalue", "sampleheader:anothervalue", "sampleheader:newvalue"}) 59 | SampleResponseDTO getOneSampleWithSingleHeaderMultipleValues(@Var("foo") String foo, @Var("bar") String bar); 60 | 61 | @Query 62 | @RequestHeader({"sampleheader : samplevalue", "anotherheader: anothervalue"}) 63 | SampleResponseDTO getOneSampleWithMultipleHeaders(@Var("foo") String foo, @Var("bar") String bar); 64 | 65 | @Query 66 | @RequestHeader({"header1 : { headerValue1 }", "header2 : { headerValue2 }"}) 67 | SampleResponseDTO getOneSampleWithMultipleDynamicHeaders( 68 | @RequestHeaderParam("headerValue1") String headerValue1, 69 | @RequestHeaderParam("headerValue2") String headerValue2, 70 | @Var("foo") String foo, @Var("bar") String bar); 71 | 72 | @Query 73 | @RequestHeader({"header1 : foo", "header2 : { headerValue2 }"}) 74 | SampleResponseDTO getOneSampleWithStaticAndDynamicHeaders( 75 | @RequestHeaderParam("headerValue2") String headerValue2, 76 | @Var("foo") String foo, @Var("bar") String bar); 77 | 78 | @Query 79 | @RequestHeader("dynamicheader : { headerValue }") 80 | SampleResponseDTO getOneSampleWithDynamicHeader(@RequestHeaderParam("headerValue") String headerValue, @Var("foo") String foo, @Var("bar") String bar); 81 | 82 | @Query 83 | @SelectionSet("{foo}") 84 | SampleResponseDTO getOneSampleCustomSelectionSet(@Var("foo") String foo, @Var("bar") String bar); 85 | 86 | @Mutation 87 | SampleResponseDTO addSample(@Var("foo") String foo, @Var("bar") String bar); 88 | 89 | @Mutation 90 | void addSample(@Var("sampleRequest") SampleRequestDTO sampleRequestDTO); 91 | 92 | @Mutation 93 | List addSampleReturnList(@Var("foo") String foo, @Var("bar") String bar); 94 | 95 | @Query 96 | OffsetDateTime getDateTime(@Var("dateTimeToReturn") OffsetDateTime dateTimeToReturn); 97 | 98 | @Query 99 | Duration getDuration(@Var("durationToReturn") Duration durationToReturn); 100 | 101 | @Query 102 | UUID getUuid(@Var("uuidToReturn") UUID uuidToReturn); 103 | 104 | @Query 105 | SuperComplexResponseType getSuperComplexStuff(@Var("superComplexSampleType") SuperComplexSampleType superComplexSampleType); 106 | 107 | @Query 108 | Optional getOneSample(@Var("sampleRequest") Optional sampleRequestDTO); 109 | 110 | @Query 111 | CyclePojo getResponseWithCycle(); 112 | 113 | @Query 114 | SampleEnum addEnum(@Var("sampleEnum") SampleEnum sampleEnum); 115 | 116 | } -------------------------------------------------------------------------------- /mocca-jaxrs2/src/main/java/com/paypal/mocca/client/MoccaJaxrsClient.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import feign.jaxrs2.JAXRSClient; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | 7 | import javax.net.ssl.HostnameVerifier; 8 | import javax.net.ssl.SSLContext; 9 | import javax.ws.rs.client.Client; 10 | import javax.ws.rs.client.ClientBuilder; 11 | import javax.ws.rs.core.Configuration; 12 | import java.security.KeyStore; 13 | import java.util.Map; 14 | import java.util.concurrent.ExecutorService; 15 | import java.util.concurrent.ScheduledExecutorService; 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * Mocca JAX-RS 2 client. In order to use a JAX-RS 2 client with Mocca, create a new instance of this class and pass it 20 | * to Mocca builder. 21 | *
22 | * See {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#client(MoccaHttpClient)} for further information 23 | * and code example. 24 | *
25 | * Instances of this class are technically supposed to support concepts like setting the read and connect timeouts per 26 | * request. However, those are ignored. The only timeouts that are used are what's established in the supplied 27 | * {@link Client}. 28 | */ 29 | final public class MoccaJaxrsClient extends MoccaHttpClient { 30 | private static Logger log = LoggerFactory.getLogger(MoccaJaxrsClient.class); 31 | 32 | /** 33 | * Create a Mocca JAX-RS 2 client using the supplied {@link Client}. 34 | * 35 | * @param client a JAX-RS client with user defined configuration 36 | */ 37 | public MoccaJaxrsClient(final Client client) { 38 | super(new JAXRSClient(new StubbornClientBuilder(client))); 39 | log.debug("Users of this may attempt to set read timeout and connect timeout per request. " + 40 | "However, those are ignored. They should be established in the supplied javax.ws.rs.Client."); 41 | } 42 | 43 | private static class StubbornClientBuilder extends ClientBuilder { 44 | private static Logger log = LoggerFactory.getLogger(StubbornClientBuilder.class); 45 | private static final String USAGE_ERR_MSG = "Only #build can be called."; 46 | 47 | private final Client client; 48 | 49 | private StubbornClientBuilder(Client client) { 50 | this.client = client; 51 | } 52 | 53 | @Override 54 | public Client build() { 55 | return client; 56 | } 57 | 58 | @Override 59 | public ClientBuilder withConfig(Configuration config) { 60 | log.warn(USAGE_ERR_MSG); 61 | return this; 62 | } 63 | 64 | @Override 65 | public ClientBuilder sslContext(SSLContext sslContext) { 66 | log.warn(USAGE_ERR_MSG); 67 | return this; 68 | } 69 | 70 | @Override 71 | public ClientBuilder keyStore(KeyStore keyStore, char[] password) { 72 | log.warn(USAGE_ERR_MSG); 73 | return this; 74 | } 75 | 76 | @Override 77 | public ClientBuilder trustStore(KeyStore trustStore) { 78 | log.warn(USAGE_ERR_MSG); 79 | return this; 80 | } 81 | 82 | @Override 83 | public ClientBuilder hostnameVerifier(HostnameVerifier verifier) { 84 | log.warn(USAGE_ERR_MSG); 85 | return this; 86 | } 87 | 88 | @Override 89 | public ClientBuilder executorService(ExecutorService executorService) { 90 | log.warn(USAGE_ERR_MSG); 91 | return this; 92 | } 93 | 94 | @Override 95 | public ClientBuilder scheduledExecutorService(ScheduledExecutorService scheduledExecutorService) { 96 | log.warn(USAGE_ERR_MSG); 97 | return this; 98 | } 99 | 100 | @Override 101 | public ClientBuilder connectTimeout(long timeout, TimeUnit unit) { 102 | return this; 103 | } 104 | 105 | @Override 106 | public ClientBuilder readTimeout(long timeout, TimeUnit unit) { 107 | return this; 108 | } 109 | 110 | @Override 111 | public Configuration getConfiguration() { 112 | return client.getConfiguration(); 113 | } 114 | 115 | @Override 116 | public ClientBuilder property(String name, Object value) { 117 | log.warn(USAGE_ERR_MSG); 118 | return this; 119 | } 120 | 121 | @Override 122 | public ClientBuilder register(Class componentClass) { 123 | log.warn(USAGE_ERR_MSG); 124 | return this; 125 | } 126 | 127 | @Override 128 | public ClientBuilder register(Class componentClass, int priority) { 129 | log.warn(USAGE_ERR_MSG); 130 | return this; 131 | } 132 | 133 | @Override 134 | public ClientBuilder register(Class componentClass, Class... contracts) { 135 | log.warn(USAGE_ERR_MSG); 136 | return this; 137 | } 138 | 139 | @Override 140 | public ClientBuilder register(Class componentClass, Map, Integer> contracts) { 141 | log.warn(USAGE_ERR_MSG); 142 | return this; 143 | } 144 | 145 | @Override 146 | public ClientBuilder register(Object component) { 147 | log.warn(USAGE_ERR_MSG); 148 | return this; 149 | } 150 | 151 | @Override 152 | public ClientBuilder register(Object component, int priority) { 153 | log.warn(USAGE_ERR_MSG); 154 | return this; 155 | } 156 | 157 | @Override 158 | public ClientBuilder register(Object component, Class... contracts) { 159 | log.warn(USAGE_ERR_MSG); 160 | return this; 161 | } 162 | 163 | @Override 164 | public ClientBuilder register(Object component, Map, Integer> contracts) { 165 | log.warn(USAGE_ERR_MSG); 166 | return this; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/test/java/com/paypal/mocca/functional/MoccaQueryTest.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.functional; 2 | 3 | import com.paypal.mocca.client.MoccaAsyncApache5Client; 4 | import com.paypal.mocca.client.MoccaClient; 5 | import com.paypal.mocca.client.MoccaOkHttpClient; 6 | import com.paypal.mocca.client.MoccaExecutorHttpClient; 7 | import com.paypal.mocca.client.MoccaMicrometerCapability; 8 | import com.paypal.mocca.client.MoccaResilience4j; 9 | import com.paypal.mocca.client.model.Book; 10 | import io.github.resilience4j.circuitbreaker.CallNotPermittedException; 11 | import io.github.resilience4j.circuitbreaker.CircuitBreaker; 12 | import io.micrometer.core.instrument.Meter; 13 | import io.micrometer.core.instrument.simple.SimpleMeterRegistry; 14 | import okhttp3.OkHttpClient; 15 | import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient; 16 | import org.apache.hc.client5.http.impl.async.HttpAsyncClients; 17 | import org.testng.annotations.Test; 18 | 19 | import javax.validation.ConstraintViolationException; 20 | import java.util.List; 21 | import java.util.concurrent.ExecutionException; 22 | import java.util.concurrent.ExecutorService; 23 | import java.util.concurrent.Executors; 24 | import java.util.function.Supplier; 25 | import java.util.stream.Collectors; 26 | 27 | import static org.junit.Assert.fail; 28 | import static org.testng.Assert.assertNotNull; 29 | import static org.testng.Assert.assertTrue; 30 | 31 | /** 32 | * Basic query tests 33 | */ 34 | public class MoccaQueryTest extends AbstractFunctionalTests { 35 | 36 | /** 37 | * Test basic graphql query on books 38 | */ 39 | @Test 40 | public void testBasicQuerySync() { 41 | List books = client.books(); 42 | runBasicQueryAndAssertions(client); 43 | } 44 | 45 | @Test 46 | public void testMicrometerSupport() throws Exception { 47 | final SimpleMeterRegistry reg = new SimpleMeterRegistry(); 48 | final BooksAppClient micrometerEnabledClient = 49 | MoccaClient.Builder.sync(getBaseUri().toString()) 50 | .addCapability(new MoccaMicrometerCapability(reg)) 51 | .build(BooksAppClient.class); 52 | 53 | runBasicQueryAndAssertions(micrometerEnabledClient); 54 | 55 | final List moccaMeters = 56 | reg.getMeters() 57 | .stream() 58 | .filter(m -> m.getId().getName().startsWith("mocca.")) 59 | .collect(Collectors.toList()); 60 | 61 | assertTrue(!moccaMeters.isEmpty(), "Expected mocca meters to be registered."); 62 | } 63 | 64 | private void runBasicQueryAndAssertions(final BooksAppClient bookClient) { 65 | List books = bookClient.books(); 66 | 67 | checkResults(books); 68 | } 69 | 70 | @Test 71 | public void testBasicQueryExecutor() throws ExecutionException, InterruptedException { 72 | ExecutorService executorService = Executors.newCachedThreadPool(); 73 | MoccaExecutorHttpClient executorClient = new MoccaExecutorHttpClient<>(new MoccaOkHttpClient(), executorService); 74 | 75 | AsyncBooksAppClient asyncClient = MoccaClient.Builder 76 | .async(getBaseUri().toString()) 77 | .client(executorClient) 78 | .build(AsyncBooksAppClient.class); 79 | 80 | List books = asyncClient.books().get(); 81 | 82 | checkResults(books); 83 | } 84 | 85 | @Test 86 | public void testBasicQueryAsync() throws ExecutionException, InterruptedException { 87 | CloseableHttpAsyncClient apacheAsyncHttpClient = HttpAsyncClients 88 | .custom() 89 | .disableAuthCaching() 90 | .disableCookieManagement() 91 | .build(); 92 | apacheAsyncHttpClient.start(); 93 | 94 | MoccaAsyncApache5Client asyncHttpClient = new MoccaAsyncApache5Client(apacheAsyncHttpClient); 95 | 96 | AsyncBooksAppClient asyncClient = MoccaClient.Builder 97 | .async(getBaseUri().toString()) 98 | .client(asyncHttpClient) 99 | .build(AsyncBooksAppClient.class); 100 | 101 | List books = asyncClient.books().get(); 102 | 103 | checkResults(books); 104 | } 105 | 106 | @Test 107 | public void testResilientQuery() { 108 | final CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("marcels-cb"); 109 | 110 | final BooksAppClient client = 111 | MoccaClient.Builder 112 | .sync(getBaseUri().toString()) 113 | .resiliency( 114 | new MoccaResilience4j.Builder() 115 | .circuitBreaker(circuitBreaker) 116 | .build() 117 | ).build(BooksAppClient.class); 118 | 119 | final Supplier> getBooks = () -> client.books(); 120 | 121 | checkResults(getBooks.get()); 122 | 123 | circuitBreaker.transitionToForcedOpenState(); 124 | try { 125 | getBooks.get(); 126 | fail("Expected circuit to be open"); 127 | } catch (final CallNotPermittedException e) { 128 | } 129 | 130 | circuitBreaker.transitionToClosedState(); 131 | checkResults(getBooks.get()); 132 | } 133 | 134 | @Test(expectedExceptions = ConstraintViolationException.class, expectedExceptionsMessageRegExp = "addAuthor\\.arg0: must not be null") 135 | public void testResilientBeanValidationQuery() { 136 | MoccaClient.Builder 137 | .sync(getBaseUri().toString()) 138 | .resiliency(new MoccaResilience4j.Builder().build()) 139 | .build(BooksAppClient.class) 140 | .addAuthor(null); 141 | } 142 | 143 | private void checkResults(List books) { 144 | assertNotNull(books); 145 | assertTrue(books.size() >= 2); 146 | //assert books are returned back 147 | assertTrue(books.stream().anyMatch(book -> book.getId() == 1)); 148 | assertTrue(books.stream().anyMatch(book -> book.getId() == 2)); 149 | assertTrue(books.stream().anyMatch(book -> "Book1".equals(book.getName()))); 150 | } 151 | 152 | } 153 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/SuperComplexResponseType.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | import com.paypal.mocca.client.SampleEnum; 4 | 5 | import java.time.Duration; 6 | import java.time.OffsetDateTime; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | import java.util.UUID; 11 | 12 | public class SuperComplexResponseType { 13 | 14 | public static class SuperComplexResponseField { 15 | 16 | int innerIntVar; 17 | String innerStringVar; 18 | boolean innerBooleanVar; 19 | List innerStringListVar; 20 | 21 | public SuperComplexResponseField setInnerIntVar(int innerIntVar) { 22 | this.innerIntVar = innerIntVar; 23 | return this; 24 | } 25 | 26 | public SuperComplexResponseField setInnerStringVar(String innerStringVar) { 27 | this.innerStringVar = innerStringVar; 28 | return this; 29 | } 30 | 31 | public SuperComplexResponseField setInnerBooleanVar(boolean innerBooleanVar) { 32 | this.innerBooleanVar = innerBooleanVar; 33 | return this; 34 | } 35 | 36 | public SuperComplexResponseField setInnerStringListVar(List innerStringListVar) { 37 | this.innerStringListVar = innerStringListVar; 38 | return this; 39 | } 40 | 41 | public int getInnerIntVar() { 42 | return innerIntVar; 43 | } 44 | 45 | public String getInnerStringVar() { 46 | return innerStringVar; 47 | } 48 | 49 | public boolean isInnerBooleanVar() { 50 | return innerBooleanVar; 51 | } 52 | 53 | public List getInnerStringListVar() { 54 | return innerStringListVar; 55 | } 56 | 57 | @Override 58 | public boolean equals(Object o) { 59 | if (this == o) return true; 60 | if (o == null || getClass() != o.getClass()) return false; 61 | 62 | SuperComplexResponseField that = (SuperComplexResponseField) o; 63 | 64 | if (innerIntVar != that.innerIntVar) return false; 65 | if (innerBooleanVar != that.innerBooleanVar) return false; 66 | if (innerStringVar != null ? !innerStringVar.equals(that.innerStringVar) : that.innerStringVar != null) return false; 67 | return innerStringListVar != null ? innerStringListVar.equals(that.innerStringListVar) : that.innerStringListVar == null; 68 | } 69 | 70 | @Override 71 | public int hashCode() { 72 | int result = innerIntVar; 73 | result = 31 * result + (innerStringVar != null ? innerStringVar.hashCode() : 0); 74 | result = 31 * result + (innerBooleanVar ? 1 : 0); 75 | result = 31 * result + (innerStringListVar != null ? innerStringListVar.hashCode() : 0); 76 | return result; 77 | } 78 | } 79 | 80 | private int intVar; 81 | private String stringVar; 82 | private boolean booleanVar; 83 | private SuperComplexResponseField complexField; 84 | private List complexListVar; 85 | private List stringListVar; 86 | private Set stringSetVar; 87 | private OffsetDateTime dateTime; 88 | private String optionalField; 89 | private Duration duration; 90 | private UUID uuid; 91 | private SampleEnum sampleEnum; 92 | 93 | public int getIntVar() { 94 | return intVar; 95 | } 96 | 97 | public SuperComplexResponseType setIntVar(int intVar) { 98 | this.intVar = intVar; 99 | return this; 100 | } 101 | 102 | public String getStringVar() { 103 | return stringVar; 104 | } 105 | 106 | public SuperComplexResponseType setStringVar(String stringVar) { 107 | this.stringVar = stringVar; 108 | return this; 109 | } 110 | 111 | public boolean isBooleanVar() { 112 | return booleanVar; 113 | } 114 | 115 | public SuperComplexResponseType setBooleanVar(boolean booleanVar) { 116 | this.booleanVar = booleanVar; 117 | return this; 118 | } 119 | 120 | public SuperComplexResponseField getComplexField() { 121 | return complexField; 122 | } 123 | 124 | public SuperComplexResponseType setComplexField(SuperComplexResponseField complexField) { 125 | this.complexField = complexField; 126 | return this; 127 | } 128 | 129 | public List getComplexListVar() { 130 | return complexListVar; 131 | } 132 | 133 | public void setComplexListVar(List complexListVar) { 134 | this.complexListVar = complexListVar; 135 | } 136 | 137 | public List getStringListVar() { 138 | return stringListVar; 139 | } 140 | 141 | public void setStringListVar(List stringListVar) { 142 | this.stringListVar = stringListVar; 143 | } 144 | 145 | public Set getStringSetVar() { 146 | return stringSetVar; 147 | } 148 | 149 | public void setStringSetVar(Set stringSetVar) { 150 | this.stringSetVar = stringSetVar; 151 | } 152 | 153 | public OffsetDateTime getDateTime() { 154 | return dateTime; 155 | } 156 | 157 | public SuperComplexResponseType setDateTime(OffsetDateTime dateTime) { 158 | this.dateTime = dateTime; 159 | return this; 160 | } 161 | 162 | public Optional getOptionalField() { 163 | return Optional.ofNullable(optionalField); 164 | } 165 | 166 | public SuperComplexResponseType setOptionalField(String optionalField) { 167 | this.optionalField = optionalField; 168 | return this; 169 | } 170 | 171 | public Duration getDuration() { 172 | return duration; 173 | } 174 | 175 | public SuperComplexResponseType setDuration(Duration duration) { 176 | this.duration = duration; 177 | return this; 178 | } 179 | 180 | public UUID getUuid() { 181 | return uuid; 182 | } 183 | 184 | public SuperComplexResponseType setUuid(UUID uuid) { 185 | this.uuid = uuid; 186 | return this; 187 | } 188 | 189 | public SampleEnum getSampleEnum() { 190 | return sampleEnum; 191 | } 192 | 193 | public SuperComplexResponseType setSampleEnum(SampleEnum sampleEnum) { 194 | this.sampleEnum = sampleEnum; 195 | return this; 196 | } 197 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin, switch paths to Windows format before running java 129 | if $cygwin ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=$((i+1)) 158 | done 159 | case $i in 160 | (0) set -- ;; 161 | (1) set -- "$args0" ;; 162 | (2) set -- "$args0" "$args1" ;; 163 | (3) set -- "$args0" "$args1" "$args2" ;; 164 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=$(save "$@") 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 184 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 185 | cd "$(dirname "$0")" 186 | fi 187 | 188 | exec "$JAVACMD" "$@" 189 | -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaDeserializer.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.lang.reflect.Array; 9 | import java.lang.reflect.Type; 10 | import java.util.Arrays; 11 | import java.util.List; 12 | import java.util.Optional; 13 | 14 | /** 15 | * Mocca GraphQL response payload deserializer 16 | * 17 | * @author fabiocarvalho777@gmail.com 18 | */ 19 | class MoccaDeserializer { 20 | 21 | // TODO In the future we could make the JSON process configurable, so application can choose between Jackson, GSON, and others 22 | // TODO There could be also value in letting application decide custom object mapper configuration 23 | private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); 24 | 25 | MoccaDeserializer() { 26 | } 27 | 28 | /** 29 | * Deserialize the response, from the provided input stream, 30 | * and bind it to an object whose type is also provided. 31 | *
32 | * In case the GraphQL operation defined an empty result (common in mutations), 33 | * then this method will return an empty optional. 34 | * 35 | * @param inputStream the stream providing the bytes to be deserialized and mapped into the response type 36 | * @param responseType the type used to instantiate a new response object 37 | * @param operationName the name of the GraphQL operation whose response will result in the object to be returned 38 | * @return an optional with the deserialized and bound response object, which can be a list or not 39 | */ 40 | Optional deserialize(final InputStream inputStream, final Type responseType, final String operationName) { 41 | return MoccaReflection.getInnerType(responseType, List.class) 42 | .map(MoccaReflection::erase) 43 | .>map(listParameterClass -> deserializeList(inputStream, listParameterClass, operationName)) 44 | .orElseGet(() -> deserializeObject(inputStream, MoccaReflection.erase(responseType), operationName)); 45 | } 46 | 47 | /* 48 | * Deserialize the response, from the provided input stream, 49 | * and bind it to a list of objects whose class is also provided. 50 | * It is expected the input stream contains a list of objects. 51 | *
52 | * In case the GraphQL operation defined an empty result (common in mutations), 53 | * then this method will return an empty optional. 54 | * 55 | * @param inputStream the stream providing the bytes to be deserialized and mapped into the response type 56 | * @param listParameterClass the class of items in the list to be deserialized and returned 57 | * @param operationName the name of the GraphQL operation whose response will result in the list to be returned 58 | * @return an optional with the deserialized and bound response list 59 | */ 60 | @SuppressWarnings({"unchecked"}) 61 | private Optional> deserializeList(final InputStream inputStream, final Class listParameterClass, final String operationName) { 62 | try { 63 | JsonNode dataNode = getDataNode(inputStream); 64 | 65 | if (!dataNode.isNull()) { 66 | JsonNode operationData = dataNode.path(operationName); 67 | Class arrayClass = (Class) Array.newInstance(listParameterClass, 0).getClass(); 68 | T[] objects = objectMapper.treeToValue(operationData, arrayClass); 69 | return Optional.of(Arrays.asList(objects)); 70 | } 71 | 72 | return Optional.empty(); 73 | } catch (IOException e) { 74 | throw new MoccaException("Error processing response JSON payload", e); 75 | } 76 | } 77 | 78 | /* 79 | * Deserialize the response, from the provided input stream, 80 | * and bind it to an object whose class is also provided. 81 | * It is expected the input stream does NOT contain a list of objects, and class is NOT a list. 82 | *
83 | * In case the GraphQL operation defined an empty result (common in mutations), 84 | * then this method will return an empty optional. 85 | * 86 | * @param inputStream the stream providing the bytes to be deserialized and mapped into the response type 87 | * @param responseClass the class (not a List) used to instantiate a new response object 88 | * @param operationName the name of the GraphQL operation whose response will result in the object to be returned 89 | * @return an optional with the deserialized and bound response object 90 | */ 91 | private Optional deserializeObject(final InputStream inputStream, final Class responseClass, final String operationName) { 92 | try { 93 | JsonNode dataNode = getDataNode(inputStream); 94 | 95 | if (!dataNode.isNull()) { 96 | JsonNode operationData = dataNode.path(operationName); 97 | if (operationData == null || operationData.toString().trim().equals("")) { 98 | throw new MoccaException("Response JSON payload does not contain data for the requested operation: " + operationName); 99 | } 100 | return Optional.ofNullable(objectMapper.treeToValue(operationData, responseClass)); 101 | } 102 | 103 | return Optional.empty(); 104 | } catch (IOException e) { 105 | throw new MoccaException("Error processing response JSON payload", e); 106 | } 107 | } 108 | 109 | private JsonNode getDataNode(final InputStream inputStream) throws IOException { 110 | JsonNode responsePayload = objectMapper.readTree(inputStream); 111 | JsonNode errorsNode = responsePayload.path("errors"); 112 | JsonNode dataNode; 113 | 114 | if (errorsNode.isMissingNode()){ 115 | dataNode = responsePayload.path("data"); 116 | if (dataNode.isMissingNode()) { 117 | throw new MoccaException("Response does not include data nor errors JSON fields"); 118 | } 119 | } else { 120 | String exceptionMessage = getErrorsMessage(errorsNode); 121 | throw new MoccaException(exceptionMessage); 122 | } 123 | 124 | return dataNode; 125 | } 126 | 127 | private static String getErrorsMessage(final JsonNode errors) { 128 | if (errors.isNull()) { 129 | return "Response contains a null errors field"; 130 | } else if(errors.size() == 0) { 131 | return "Response contains an empty errors list"; 132 | } else if(errors.size() == 1) { 133 | return errors.get(0).path("message").asText(); 134 | } else { 135 | return errors.toString(); 136 | } 137 | } 138 | 139 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaFeignContract.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.annotation.*; 4 | import feign.*; 5 | 6 | import java.lang.reflect.Parameter; 7 | import java.util.ArrayList; 8 | import java.util.Collection; 9 | import java.util.LinkedHashMap; 10 | import java.util.Map; 11 | 12 | /** 13 | * Mocca Feign contract class, allowing Mocca to 14 | * dynamically configure target interfaces 15 | * with the necessary Feign annotations 16 | * 17 | * @author crankydillo@gmail.com 18 | */ 19 | class MoccaFeignContract extends AlwaysEncodeBodyContract { 20 | 21 | MoccaFeignContract() { 22 | 23 | // Mocca annotations support 24 | super.registerMethodAnnotation(Query.class, (annotation, metadata) -> registerJsonMediaTypes(metadata)); 25 | super.registerMethodAnnotation(Mutation.class, (annotation, metadata) -> registerJsonMediaTypes(metadata)); 26 | super.registerParameterAnnotation(Var.class, this::registerVarParam); 27 | 28 | // HTTP headers support 29 | super.registerClassAnnotation(RequestHeader.class, this::registerHeaderClass); 30 | super.registerMethodAnnotation(RequestHeader.class, this::registerHeaderMethod); 31 | super.registerParameterAnnotation(RequestHeaderParam.class, this::registerHeaderParam); 32 | } 33 | 34 | //TODO: Need to add unit tests for since contract has logic now 35 | //TODO: JSON values are not supported yet. Need to add encoding feature. 36 | 37 | /** 38 | * Registering content-type and accept HTTP headers, 39 | * set to JSON, as always required for GraphQL request and response payloads 40 | * 41 | * @param metadata method metadata 42 | */ 43 | private void registerJsonMediaTypes(MethodMetadata metadata) { 44 | metadata.template() 45 | .method(Request.HttpMethod.POST) 46 | .header("Content-Type", "application/json") 47 | .header("Accept", "application/json"); 48 | } 49 | 50 | /** 51 | * Registering Mocca Var annotated parameter 52 | * 53 | * @param varAnnotation Var annotation 54 | * @param metadata method metadata 55 | */ 56 | private void registerVarParam(Var varAnnotation, MethodMetadata metadata, int paramIndex) { 57 | final String variableName = varAnnotation.value(); 58 | if (varAnnotation.raw()) { 59 | Util.checkState((variableName == null || variableName.trim().equals("")) && (varAnnotation.ignore() == null || varAnnotation.ignore().length == 0), 60 | "Mocca @Var `value` and `ignore` properties set at %s.%s parameter %d must not be set if raw is set to true", 61 | metadata.method().getDeclaringClass().getName(), metadata.method().getName(), paramIndex); 62 | } else { 63 | Util.checkState(variableName != null && !variableName.trim().isEmpty(), 64 | "Mocca @Var `value` set at %s.%s parameter %d cannot be null nor blank, unless if raw is set to true", 65 | metadata.method().getDeclaringClass().getName(), metadata.method().getName(), paramIndex); 66 | } 67 | nameParam(metadata, variableName, paramIndex); 68 | } 69 | 70 | /** 71 | * Registering {@link RequestHeader} annotation at class level. Followed same approach as in 72 | * Feign's default contract. 73 | * 74 | * @param header RequestHeader annotation 75 | * @param metadata Method metadata 76 | */ 77 | private void registerHeaderClass(RequestHeader header, MethodMetadata metadata) { 78 | final String[] headersOnType = header.value(); 79 | Util.checkState(headersOnType.length > 0, "RequestHeader annotation was empty on type %s.", 80 | metadata.configKey()); 81 | final Map> headers = toMap(headersOnType); 82 | verifyNoDynamicValue(headers); 83 | headers.putAll(metadata.template().headers()); 84 | metadata.template().headers(null); // to clear 85 | metadata.template().headers(headers); 86 | } 87 | 88 | /** 89 | * Registering {@link RequestHeader} annotation at method level. Followed same approach as in 90 | * Feign's default contract. 91 | * 92 | * @param header RequestHeader annotation 93 | * @param metadata Method metadata 94 | */ 95 | private void registerHeaderMethod(RequestHeader header, MethodMetadata metadata) { 96 | final String[] headersOnMethod = header.value(); 97 | Util.checkState(headersOnMethod.length > 0, "RequestHeader annotation was empty on method %s.", 98 | metadata.configKey()); 99 | metadata.template().headers(toMap(headersOnMethod)); 100 | } 101 | 102 | /** 103 | * Registering {@link RequestHeaderParam} annotation at method level. Followed same approach as in 104 | * Feign's default contract. 105 | * 106 | * @param headerParam RequestHeaderParam annotation 107 | * @param metadata Method metadata 108 | */ 109 | private void registerHeaderParam(RequestHeaderParam headerParam, MethodMetadata metadata, int paramIndex) { 110 | final String annotationName = headerParam.value(); 111 | final Parameter parameter = metadata.method().getParameters()[paramIndex]; 112 | final String name; 113 | if (Util.emptyToNull(annotationName) == null && parameter.isNamePresent()) { 114 | name = parameter.getName(); 115 | } else { 116 | name = annotationName; 117 | } 118 | Util.checkState(Util.emptyToNull(name) != null, "Param annotation was empty on param %s.", 119 | paramIndex); 120 | nameParam(metadata, name, paramIndex); 121 | } 122 | 123 | private static Map> toMap(String[] input) { 124 | final Map> result = 125 | new LinkedHashMap<>(input.length); 126 | for (final String header : input) { 127 | final int colon = header.indexOf(':'); 128 | final String name = header.substring(0, colon); 129 | if (!result.containsKey(name)) { 130 | result.put(name, new ArrayList<>(1)); 131 | } 132 | result.get(name).add(header.substring(colon + 1).trim()); 133 | } 134 | return result; 135 | } 136 | 137 | /** 138 | * Verifies that no dynamic value is set at the class level. Dynamic 139 | * value is anything which starts with "{" character. Of course this 140 | * will create problem for JSON values for which we have to use encoding 141 | * feature. 142 | * 143 | * @param headers all headers 144 | */ 145 | private void verifyNoDynamicValue(Map> headers) { 146 | headers.entrySet().stream() 147 | .flatMap(e -> e.getValue().stream()) 148 | .forEach(v -> { 149 | if (v.startsWith("{")) { 150 | throw new MoccaException("Header value:" + v + " at class level cannot be dynamic"); 151 | } 152 | }); 153 | } 154 | 155 | } 156 | -------------------------------------------------------------------------------- /mocca-resilience4j/src/main/java/com/paypal/mocca/client/MoccaResilience4j.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import io.github.resilience4j.bulkhead.Bulkhead; 4 | import io.github.resilience4j.circuitbreaker.CircuitBreaker; 5 | import io.github.resilience4j.feign.FeignDecorators; 6 | import io.github.resilience4j.feign.Resilience4jFeign; 7 | import io.github.resilience4j.ratelimiter.RateLimiter; 8 | import io.github.resilience4j.retry.Retry; 9 | 10 | import java.util.function.Function; 11 | import java.util.function.Predicate; 12 | 13 | /** 14 | * Mocca supports Resilience4j-based resilience. 15 | *
16 | * The example below shows how to configure resilience features in a Mocca client using Resilience4j: 17 | *

 18 |  * import io.github.resilience4j.circuitbreaker.CircuitBreaker;
 19 |  * import com.paypal.mocca.client.MoccaResilience4j;
 20 |  *
 21 |  * ...
 22 |  *
 23 |  * CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("BooksAppClient-cb");
 24 |  *
 25 |  * MoccaResilience4j moccaResilience = new MoccaResilience4j.Builder()
 26 |  *     .circuitBreaker(circuitBreaker)
 27 |  *     .build();
 28 |  *
 29 |  * BooksAppClient client = MoccaClient.Builder
 30 |  *     .sync("localhost:8080/booksapp")
 31 |  *     .resiliency(moccaResilience)
 32 |  *     .build(BooksAppClient.class);
 33 |  * 
34 | * Notice that {@link Builder} is used to create a {@link MoccaResilience4j} object containing the application specific Resilience4j configuration. 35 | *
36 | * A few important notes: 37 | *
    38 | *
  1. Mocca Resilience4j feature is only supported by clients created with {@link com.paypal.mocca.client.MoccaClient.Builder.SyncBuilder#sync(String)} (as seen in the example above). Clients created with {@link com.paypal.mocca.client.MoccaClient.Builder.AsyncBuilder#async(String)} don't support Resilience4j at the moment.
  2. 39 | *
  3. Although the example above only shows the setting of a circuit breaker, the following additional Resilience4j features are supported. 40 | *
      41 | *
    1. Retry
    2. 42 | *
    3. Rate limiting
    4. 43 | *
    5. Bulkhead
    6. 44 | *
    7. Fallback
    8. 45 | *
    46 | *
  4. 47 | *
  5. The order of registering each resilience feature in {@link MoccaResilience4j} matters. More details at the next subsection.
  6. 48 | *
49 | *
50 | *

Resilience features execution order

51 | * It is very important to state that the order in which resilience features are registered dictates the execution order. 52 | *
53 | * As an example, in the sample code below, {@code rateLimiter} in {@code moccaResilience1} will be executed before {@code circuitBreaker}. In other words, even if the circuit is open, the rate of calls will still be controlled and limited. However, in {@code moccaResilience2}, once the circuit is open, rate limiting will not be executed. 54 | *

 55 |  * MoccaResilience4j moccaResilience1 = new MoccaResilience4j.Builder()
 56 |  *     .rateLimiter(rateLimiter)
 57 |  *     .circuitBreaker(circuitBreaker)
 58 |  *     .build();
 59 |  *
 60 |  * MoccaResilience4j moccaResilience2 = new MoccaResilience4j.Builder()
 61 |  *     .circuitBreaker(circuitBreaker)
 62 |  *     .rateLimiter(rateLimiter)
 63 |  *     .build();
 64 |  * 
65 | * Notice the instantiation and configuration of {@code rateLimiter} and {@code circuitBreaker} were omitted for brevity. 66 | * 67 | * @author crankydillo@gmail.com amansingh21197@gmail.com 68 | */ 69 | public final class MoccaResilience4j extends MoccaResiliency { 70 | 71 | private MoccaResilience4j(final MoccaResilience4jFeignDecorators decorators) { 72 | super(Resilience4jFeign.builder(decorators)); 73 | } 74 | 75 | public static class Builder { 76 | private FeignDecorators.Builder feignBuilder; 77 | 78 | Builder(final FeignDecorators.Builder feignBuilder) { 79 | this.feignBuilder = feignBuilder; 80 | } 81 | 82 | public Builder() { 83 | this(FeignDecorators.builder()); 84 | } 85 | 86 | public MoccaResilience4j build() { 87 | return new MoccaResilience4j(new MoccaResilience4jFeignDecorators(feignBuilder.build())); 88 | } 89 | 90 | /** 91 | * Adds a {@link Retry} to the decorator chain. 92 | * 93 | * @param retry the retry object to be set in this builder 94 | * @return this builder 95 | */ 96 | public Builder retry(final Retry retry) { 97 | this.feignBuilder = feignBuilder.withRetry(retry); 98 | return this; 99 | } 100 | 101 | /** 102 | * Adds a {@link CircuitBreaker} to the decorator chain. 103 | * 104 | * @param circuitBreaker the circuit breaker object to be set in this builder 105 | * @return this builder 106 | */ 107 | public Builder circuitBreaker(final CircuitBreaker circuitBreaker) { 108 | this.feignBuilder = feignBuilder.withCircuitBreaker(circuitBreaker); 109 | return this; 110 | } 111 | 112 | /** 113 | * Adds a {@link RateLimiter} to the decorator chain. 114 | * 115 | * @param rateLimiter the rate limiter object to be set in this builder 116 | * @return this builder 117 | */ 118 | public Builder rateLimiter(final RateLimiter rateLimiter) { 119 | this.feignBuilder = feignBuilder.withRateLimiter(rateLimiter); 120 | return this; 121 | } 122 | 123 | /** 124 | * Adds a {@link Bulkhead} to the decorator chain. 125 | * 126 | * @param bulkhead the bulkhead object to be set in this builder 127 | * @return this builder 128 | */ 129 | public Builder bulkhead(final Bulkhead bulkhead) { 130 | this.feignBuilder = feignBuilder.withBulkhead(bulkhead); 131 | return this; 132 | } 133 | 134 | /** 135 | * Adds a Fallback to the decorator chain 136 | * 137 | * @param fallBack the fallback object to be set to in this builder 138 | * @return this builder 139 | */ 140 | public Builder fallback(final Object fallBack) { 141 | this.feignBuilder = feignBuilder.withFallback(fallBack); 142 | return this; 143 | } 144 | 145 | /** 146 | * Adds a Fallback to the decorator chain using the exception filter predicate 147 | * 148 | * @param fallBack the fallback object to be set to in this builder 149 | * @param filter predicate for the exception filter 150 | * @return this builder 151 | */ 152 | public Builder fallback(final Object fallBack, Predicate filter) { 153 | this.feignBuilder = feignBuilder.withFallback(fallBack, filter); 154 | return this; 155 | } 156 | 157 | /** 158 | * Adds a Fallback to the decorator chain using the exception filter class 159 | * 160 | * @param fallBack the fallback object to be set to in this builder 161 | * @param filter class for the exception filter 162 | * @return this builder 163 | */ 164 | public Builder fallback(final Object fallBack, Class filter) { 165 | this.feignBuilder = feignBuilder.withFallback(fallBack, filter); 166 | return this; 167 | } 168 | 169 | /** 170 | * Adds a Fallback factory to the decorator chain 171 | * 172 | * @param fallBackFactory the fallback factory function to be set in this builder 173 | * @return this builder 174 | */ 175 | public Builder fallbackFactory(Function fallBackFactory ) { 176 | this.feignBuilder = feignBuilder.withFallbackFactory(fallBackFactory); 177 | return this; 178 | } 179 | 180 | /** 181 | * Adds a Fallback factory to the decorator chain using the exception filter class 182 | * 183 | * @param fallbackFactory the fallback factory function to be set in this builder 184 | * @param filter class for the exception filter 185 | * @return this builder 186 | */ 187 | public Builder fallbackFactory(Function fallbackFactory, 188 | Class filter) { 189 | this.feignBuilder = feignBuilder.withFallbackFactory(fallbackFactory, filter); 190 | return this; 191 | } 192 | 193 | /** 194 | * Adds a Fallback factory to the decorator chain using the exception filter predicate 195 | * 196 | * @param fallbackFactory the fallback factory function to be set in this builder 197 | * @param filter predicate for the exception filter 198 | * @return this builder 199 | */ 200 | public Builder fallbackFactory(Function fallbackFactory, 201 | Predicate filter) { 202 | this.feignBuilder = feignBuilder.withFallbackFactory(fallbackFactory, filter); 203 | return this; 204 | } 205 | 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /mocca-client/src/test/java/com/paypal/mocca/client/sample/SuperComplexSampleType.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client.sample; 2 | 3 | import com.paypal.mocca.client.SampleEnum; 4 | 5 | import java.time.Duration; 6 | import java.time.OffsetDateTime; 7 | import java.util.List; 8 | import java.util.Optional; 9 | import java.util.Set; 10 | import java.util.StringJoiner; 11 | import java.util.UUID; 12 | 13 | public class SuperComplexSampleType { 14 | 15 | public static class SuperComplexField { 16 | int innerIntVar; 17 | String innerStringVar; 18 | boolean innerBooleanVar; 19 | List innerStringListVar; 20 | SuperComplexField innerComplexVar; 21 | List innerComplexListVar; 22 | 23 | public SuperComplexField(int innerIntVar, String innerStringVar, boolean innerBooleanVar, 24 | List innerStringListVar, SuperComplexField innerComplexVar, 25 | List innerComplexListVar) { 26 | this.innerIntVar = innerIntVar; 27 | this.innerStringVar = innerStringVar; 28 | this.innerBooleanVar = innerBooleanVar; 29 | this.innerStringListVar = innerStringListVar; 30 | this.innerComplexVar = innerComplexVar; 31 | this.innerComplexListVar = innerComplexListVar; 32 | } 33 | 34 | public int getInnerIntVar() { 35 | return innerIntVar; 36 | } 37 | 38 | public SuperComplexField setInnerIntVar(int innerIntVar) { 39 | this.innerIntVar = innerIntVar; 40 | return this; 41 | } 42 | 43 | public String getInnerStringVar() { 44 | return innerStringVar; 45 | } 46 | 47 | public SuperComplexField setInnerStringVar(String innerStringVar) { 48 | this.innerStringVar = innerStringVar; 49 | return this; 50 | } 51 | 52 | public boolean isInnerBooleanVar() { 53 | return innerBooleanVar; 54 | } 55 | 56 | public SuperComplexField setInnerBooleanVar(boolean innerBooleanVar) { 57 | this.innerBooleanVar = innerBooleanVar; 58 | return this; 59 | } 60 | 61 | public List getInnerStringListVar() { 62 | return innerStringListVar; 63 | } 64 | 65 | public void setInnerStringListVar(List innerStringListVar) { 66 | this.innerStringListVar = innerStringListVar; 67 | } 68 | 69 | public SuperComplexField getInnerComplexVar() { 70 | return innerComplexVar; 71 | } 72 | 73 | public void setInnerComplexVar(SuperComplexField innerComplexVar) { 74 | this.innerComplexVar = innerComplexVar; 75 | } 76 | 77 | public List getInnerComplexListVar() { 78 | return innerComplexListVar; 79 | } 80 | 81 | public void setInnerComplexListVar(List innerComplexListVar) { 82 | this.innerComplexListVar = innerComplexListVar; 83 | } 84 | 85 | @Override 86 | public boolean equals(Object o) { 87 | if (this == o) return true; 88 | if (o == null || getClass() != o.getClass()) return false; 89 | 90 | SuperComplexField that = (SuperComplexField) o; 91 | 92 | if (innerIntVar != that.innerIntVar) return false; 93 | if (innerBooleanVar != that.innerBooleanVar) return false; 94 | if (innerStringVar != null ? !innerStringVar.equals(that.innerStringVar) : that.innerStringVar != null) return false; 95 | if (innerStringListVar != null ? !innerStringListVar.equals(that.innerStringListVar) : that.innerStringListVar != null) return false; 96 | if (innerComplexVar != null ? !innerComplexVar.equals(that.innerComplexVar) : that.innerComplexVar != null) return false; 97 | return innerComplexListVar != null ? innerComplexListVar.equals(that.innerComplexListVar) : that.innerComplexListVar == null; 98 | } 99 | 100 | @Override 101 | public int hashCode() { 102 | int result = innerIntVar; 103 | result = 31 * result + (innerStringVar != null ? innerStringVar.hashCode() : 0); 104 | result = 31 * result + (innerBooleanVar ? 1 : 0); 105 | result = 31 * result + (innerStringListVar != null ? innerStringListVar.hashCode() : 0); 106 | result = 31 * result + (innerComplexVar != null ? innerComplexVar.hashCode() : 0); 107 | result = 31 * result + (innerComplexListVar != null ? innerComplexListVar.hashCode() : 0); 108 | return result; 109 | } 110 | 111 | @Override 112 | public String toString() { 113 | return new StringJoiner(",\n", SuperComplexField.class.getSimpleName() + "[\n", "]") 114 | .add("innerIntVar=" + innerIntVar) 115 | .add("innerStringVar='" + innerStringVar + "'") 116 | .add("innerBooleanVar=" + innerBooleanVar) 117 | .add("innerStringListVar=" + innerStringListVar) 118 | .add("innerComplexVar=" + innerComplexVar) 119 | .add("innerComplexListVar=" + innerComplexListVar) 120 | .toString(); 121 | } 122 | } 123 | 124 | private int intVar; 125 | private String stringVar; 126 | private boolean booleanVar; 127 | private SuperComplexField complexField; 128 | private List complexListVar; 129 | private List stringListVar; 130 | private Set stringSetVar; 131 | private OffsetDateTime dateTime; 132 | private String optionalField; 133 | private Duration duration; 134 | private UUID uuid; 135 | private SampleEnum sampleEnum; 136 | 137 | public SuperComplexSampleType(int intVar, String stringVar, boolean booleanVar, SuperComplexField complexField, 138 | List complexListVar, List stringListVar, 139 | Set stringSetVar) { 140 | this.intVar = intVar; 141 | this.stringVar = stringVar; 142 | this.booleanVar = booleanVar; 143 | this.complexField = complexField; 144 | this.complexListVar = complexListVar; 145 | this.stringListVar = stringListVar; 146 | this.stringSetVar = stringSetVar; 147 | } 148 | 149 | public int getIntVar() { 150 | return intVar; 151 | } 152 | 153 | public SuperComplexSampleType setIntVar(int intVar) { 154 | this.intVar = intVar; 155 | return this; 156 | } 157 | 158 | public String getStringVar() { 159 | return stringVar; 160 | } 161 | 162 | public SuperComplexSampleType setStringVar(String stringVar) { 163 | this.stringVar = stringVar; 164 | return this; 165 | } 166 | 167 | public boolean isBooleanVar() { 168 | return booleanVar; 169 | } 170 | 171 | public SuperComplexSampleType setBooleanVar(boolean booleanVar) { 172 | this.booleanVar = booleanVar; 173 | return this; 174 | } 175 | 176 | public SuperComplexField getComplexField() { 177 | return complexField; 178 | } 179 | 180 | public SuperComplexSampleType setComplexField(SuperComplexField complexField) { 181 | this.complexField = complexField; 182 | return this; 183 | } 184 | 185 | public List getComplexListVar() { 186 | return complexListVar; 187 | } 188 | 189 | public void setComplexListVar(List complexListVar) { 190 | this.complexListVar = complexListVar; 191 | } 192 | 193 | public List getStringListVar() { 194 | return stringListVar; 195 | } 196 | 197 | public void setStringListVar(List stringListVar) { 198 | this.stringListVar = stringListVar; 199 | } 200 | 201 | public Set getStringSetVar() { 202 | return stringSetVar; 203 | } 204 | 205 | public void setStringSetVar(Set stringSetVar) { 206 | this.stringSetVar = stringSetVar; 207 | } 208 | 209 | public OffsetDateTime getDateTime() { 210 | return dateTime; 211 | } 212 | 213 | public SuperComplexSampleType setDateTime(OffsetDateTime dateTime) { 214 | this.dateTime = dateTime; 215 | return this; 216 | } 217 | 218 | public Optional getOptionalField() { 219 | return Optional.ofNullable(optionalField); 220 | } 221 | 222 | public SuperComplexSampleType setOptionalField(String optionalField) { 223 | this.optionalField = optionalField; 224 | return this; 225 | } 226 | 227 | public Duration getDuration() { 228 | return duration; 229 | } 230 | 231 | public SuperComplexSampleType setDuration(Duration duration) { 232 | this.duration = duration; 233 | return this; 234 | } 235 | 236 | public UUID getUuid() { 237 | return uuid; 238 | } 239 | 240 | public SuperComplexSampleType setUuid(UUID uuid) { 241 | this.uuid = uuid; 242 | return this; 243 | } 244 | 245 | public SampleEnum getSampleEnum() { 246 | return sampleEnum; 247 | } 248 | 249 | public SuperComplexSampleType setSampleEnum(SampleEnum sampleEnum) { 250 | this.sampleEnum = sampleEnum; 251 | return this; 252 | } 253 | 254 | @Override 255 | public String toString() { 256 | return new StringJoiner(", ", SuperComplexSampleType.class.getSimpleName() + "[", "]") 257 | .add("intVar=" + intVar) 258 | .add("stringVar='" + stringVar + "'") 259 | .add("booleanVar=" + booleanVar) 260 | .add("complexField=" + complexField) 261 | .add("complexListVar=" + complexListVar) 262 | .add("stringListVar=" + stringListVar) 263 | .add("dateTime=" + dateTime) 264 | .add("optionalField='" + optionalField + "'") 265 | .add("duration=" + duration) 266 | .add("uuid=" + uuid) 267 | .add("sampleEnum=" + sampleEnum) 268 | .toString(); 269 | } 270 | 271 | } -------------------------------------------------------------------------------- /mocca-client/src/main/java/com/paypal/mocca/client/MoccaFeignEncoder.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.client; 2 | 3 | import com.paypal.mocca.client.MoccaSerializer.Variable; 4 | import com.paypal.mocca.client.annotation.Mutation; 5 | import com.paypal.mocca.client.annotation.Query; 6 | import com.paypal.mocca.client.annotation.SelectionSet; 7 | import com.paypal.mocca.client.annotation.Var; 8 | import com.paypal.mocca.client.annotation.RequestHeaderParam; 9 | import feign.RequestTemplate; 10 | import feign.Response; 11 | import feign.codec.EncodeException; 12 | import feign.codec.Encoder; 13 | import org.slf4j.Logger; 14 | import org.slf4j.LoggerFactory; 15 | 16 | import javax.validation.ConstraintViolation; 17 | import javax.validation.ConstraintViolationException; 18 | import javax.validation.Validation; 19 | import javax.validation.Validator; 20 | import java.io.IOException; 21 | import java.lang.annotation.Annotation; 22 | import java.lang.reflect.Method; 23 | import java.lang.reflect.Parameter; 24 | import java.lang.reflect.Type; 25 | import java.nio.charset.Charset; 26 | import java.util.ArrayList; 27 | import java.util.List; 28 | import java.util.Set; 29 | 30 | /** 31 | * Mocca Feign encoder, responsible for serializing the request payload 32 | * 33 | * @author fabiocarvalho777@gmail.com 34 | */ 35 | class MoccaFeignEncoder implements Encoder { 36 | private static final Logger logger = LoggerFactory.getLogger(MoccaFeignEncoder.class); 37 | 38 | private final MoccaSerializer moccaSerializer = new MoccaSerializer(); 39 | /** 40 | * Notice that this Validator is in the older javax.validation package not newer jakarta.validation package. 41 | * Please refer to {@link javax.validation.Validator} for more information. 42 | */ 43 | private Validator validator; 44 | 45 | // The client is only used for validation and is not needed for encoding. 46 | private MoccaClient client; 47 | 48 | MoccaFeignEncoder() { 49 | try { 50 | Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); 51 | this.validator = validator; 52 | } catch (Exception e) { 53 | // No validation provider found 54 | logger.warn("No implementation of javax.validation.Validator was found on the classpath. Mocca will be unable to perform request parameters validation."); 55 | } 56 | } 57 | 58 | public void setClient(MoccaClient client) { 59 | this.client = client; 60 | } 61 | 62 | @Override 63 | public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException { 64 | 65 | if (bodyType != Object[].class) { 66 | throw new MoccaException("Unexpected body object type: " + bodyType.getTypeName()); 67 | } 68 | Object[] parameters = (Object[]) object; 69 | 70 | try { 71 | final Type responseType = template.methodMetadata().returnType(); 72 | final String operationName = getOperationName(template); 73 | final OperationType operationType = getOperationType(template); 74 | final SelectionSet selectionSet = getSelectionSet(template); 75 | final List variables = getVariables(parameters, template); 76 | if (validator != null) { 77 | validateVariables(parameters, template); 78 | } 79 | final byte[] data = moccaSerializer.serialize(variables, responseType, operationName, operationType, selectionSet); 80 | template.body(data, Charset.defaultCharset()); 81 | 82 | } catch (IOException e) { 83 | throw new MoccaException("An error happened when serializing the request payload from type " + bodyType.getTypeName(), e); 84 | } 85 | } 86 | 87 | /** 88 | * Validates the client request using the bean validation 89 | * API for validating all the parameters in a method invocation. 90 | * The Feign request object points to the method being invoked but 91 | * not the client object itself. This is why we have to set the client 92 | * as a field in the encoder. 93 | * @param parameters all the parameters passed to the client method 94 | * @param template the Feign request template object 95 | */ 96 | void validateVariables(Object[] parameters, RequestTemplate template) { 97 | 98 | Method method = template.methodMetadata().method(); 99 | Set> violationSet = validator.forExecutables().validateParameters(client, method, parameters); 100 | 101 | if (violationSet.size() > 0) { 102 | throw new ConstraintViolationException(violationSet); 103 | } 104 | } 105 | 106 | /** 107 | * Returns the operation name associated with a Feign response object 108 | * 109 | * @param response the Feign response object 110 | * @return the operation name associated with a Feign response object 111 | */ 112 | static String getOperationName(Response response) { 113 | return getOperationName(response.request().requestTemplate()); 114 | } 115 | 116 | /** 117 | * Returns the operation name associated with a Feign request template object 118 | * 119 | * @param requestTemplate the Feign request template object 120 | * @return the operation name associated with a Feign request template object 121 | */ 122 | static String getOperationName(RequestTemplate requestTemplate) { 123 | final String methodName = requestTemplate.methodMetadata().method().getName(); 124 | final Annotation operationAnnotation = getOperationAnnotation(requestTemplate); 125 | final String operationName; 126 | if (operationAnnotation instanceof Query) { 127 | Query annotation = (Query) operationAnnotation; 128 | operationName = annotation.name().equals(Query.UNDEFINED) ? methodName : annotation.name(); 129 | } else if (operationAnnotation instanceof Mutation) { 130 | Mutation annotation = (Mutation) operationAnnotation; 131 | operationName = annotation.name().equals(Mutation.UNDEFINED) ? methodName : annotation.name(); 132 | } else { 133 | throw new IllegalStateException("The operation method " + methodName + " is not annotated with an unsupported operation annotation " + operationAnnotation.getClass().getName()); 134 | } 135 | return operationName; 136 | } 137 | 138 | /** 139 | * Returns the operation type associated with a Feign request template object 140 | * 141 | * @param requestTemplate the Feign request template object 142 | * @return the operation type associated with a Feign request template object 143 | */ 144 | static OperationType getOperationType(RequestTemplate requestTemplate) { 145 | Annotation operationAnnotation = getOperationAnnotation(requestTemplate); 146 | return OperationType.valueOf(operationAnnotation); 147 | } 148 | 149 | private static Annotation getOperationAnnotation(RequestTemplate requestTemplate) { 150 | Method method = requestTemplate.methodMetadata().method(); 151 | Query query = method.getAnnotation(Query.class); 152 | Mutation mutation = method.getAnnotation(Mutation.class); 153 | if (query == null && mutation == null) { 154 | throw new MoccaException("The operation method " + method.getName() + " is not annotated with " + Query.class.getName() + " nor " + Mutation.class.getName()); 155 | } 156 | if (query != null && mutation != null) { 157 | throw new MoccaException("The operation method " + method.getName() + " is not annotated with both " + Query.class.getName() + " and " + Mutation.class.getName()); 158 | } 159 | return query != null ? query : mutation; 160 | } 161 | 162 | /** 163 | * Returns the selection set associated with a Feign request template object 164 | * 165 | * @param requestTemplate the Feign request template object 166 | * @return the selection set associated with a Feign request template object 167 | */ 168 | static SelectionSet getSelectionSet(RequestTemplate requestTemplate) { 169 | Method method = requestTemplate.methodMetadata().method(); 170 | return method.getAnnotation(SelectionSet.class); 171 | } 172 | 173 | /** 174 | * Returns a list containing the operation variables associated with a Feign request template object. 175 | * Operation variables are the operation method parameters annotated with {@link com.paypal.mocca.client.annotation.Var}. 176 | * If the request method doesn't have operation variables, an empty list is returned. 177 | * The list is ordered according to {@code parameters} order. 178 | * 179 | * @param parameters GraphQL operation method parameter values 180 | * @param requestTemplate the Feign request template object 181 | * @return a list containing the operation variables 182 | * associated with a Feign request template object 183 | */ 184 | private static List getVariables(Object[] parameters, RequestTemplate requestTemplate) { 185 | Parameter[] parametersMetadata = requestTemplate.methodMetadata().method().getParameters(); 186 | List variables = new ArrayList<>(parameters.length); 187 | for (int i = 0; i < parameters.length; i++) { 188 | 189 | // TODO We could make it configurable, leaving for the user to decide if null variables should 190 | // be omitted or just set with null value. For now we will just skip them. 191 | if (parameters[i] == null) { 192 | logger.debug("Skipping parameter at position {} as it is set to null", i); 193 | continue; 194 | } 195 | 196 | Parameter parameterMetadata = parametersMetadata[i]; 197 | Var varAnnotation = parameterMetadata.getAnnotation(Var.class); 198 | // FIXME It would be better if this check happened at client definition time, instead of request time 199 | if (varAnnotation == null) { 200 | if (parameterMetadata.getAnnotation(RequestHeaderParam.class) == null) { 201 | final String method = requestTemplate.methodMetadata().method().getName(); 202 | throw new MoccaException("Invalid GraphQL operation method " + method + ", make sure all its parameters are annotated with one Mocca annotation"); 203 | } 204 | } else { 205 | if (parameterMetadata.getAnnotation(RequestHeaderParam.class) != null) { 206 | final String method = requestTemplate.methodMetadata().method().getName(); 207 | throw new MoccaException("Invalid GraphQL operation method " + method + ", make sure all its parameters are annotated with one Mocca annotation"); 208 | } 209 | Variable variable = new Variable(parameters[i], parameterMetadata.getParameterizedType(), varAnnotation); 210 | variables.add(variable); 211 | } 212 | } 213 | return variables; 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /mocca-functional-tests/src/main/java/com/paypal/mocca/server/GraphQLEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.paypal.mocca.server; 2 | 3 | import com.fasterxml.jackson.core.JsonParseException; 4 | import com.fasterxml.jackson.core.type.TypeReference; 5 | import com.fasterxml.jackson.databind.JsonMappingException; 6 | import com.fasterxml.jackson.databind.ObjectMapper; 7 | import graphql.ExecutionInput; 8 | import graphql.ExecutionResult; 9 | import graphql.ExecutionResultImpl; 10 | import graphql.GraphQL; 11 | import graphql.GraphQLError; 12 | import graphql.GraphqlErrorException; 13 | import org.apache.commons.lang3.StringUtils; 14 | import org.slf4j.Logger; 15 | import org.slf4j.LoggerFactory; 16 | 17 | import javax.activation.MimeType; 18 | import javax.activation.MimeTypeParseException; 19 | import javax.servlet.http.HttpServletRequest; 20 | import javax.ws.rs.Consumes; 21 | import javax.ws.rs.HeaderParam; 22 | import javax.ws.rs.NotSupportedException; 23 | import javax.ws.rs.POST; 24 | import javax.ws.rs.Path; 25 | import javax.ws.rs.Produces; 26 | import javax.ws.rs.container.AsyncResponse; 27 | import javax.ws.rs.container.Suspended; 28 | import javax.ws.rs.core.Context; 29 | import javax.ws.rs.core.MediaType; 30 | import javax.ws.rs.core.Response; 31 | import java.io.IOException; 32 | import java.util.HashMap; 33 | import java.util.List; 34 | import java.util.Map; 35 | import java.util.concurrent.CompletableFuture; 36 | import java.util.concurrent.CompletionStage; 37 | 38 | /** 39 | * GraphQL endpoint for the server 40 | */ 41 | @Path("graphql") 42 | public class GraphQLEndpoint { 43 | /** 44 | * Logger 45 | */ 46 | private static final Logger LOGGER = LoggerFactory.getLogger(GraphQLEndpoint.class); 47 | /** 48 | * GraphQL media type 49 | */ 50 | private static final String MEDIA_TYPE_GRAPHQL = "application/graphql"; 51 | /** 52 | * Jackson object mapper 53 | */ 54 | private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); 55 | /** 56 | * Main GraphQL instance which is shared by graphql-java library. Need it to be static 57 | * as Jersey creates new instance of resource class for each request. 58 | */ 59 | private static final GraphQL graphQL = GraphQLFactory.getInstance(OBJECT_MAPPER); 60 | /** 61 | * Cache to keep Content-Type to MimeType mappings 62 | */ 63 | private final Map mimeTypeMap = new HashMap<>(); 64 | 65 | /** 66 | * POST method which handles all GraphQL requests. GraphQL requests are always 67 | * sent to single URI '/graphql' 68 | * 69 | * @param asyncResponse Async response given by JAX-RS 70 | * @param contentType Content Type of the request. Supports both application/json and 71 | * application/graphql 72 | * @param body Query or Mutation string in request body 73 | * @param request The request object used for passing as a context to graphql 74 | * processor 75 | */ 76 | @Consumes({MediaType.APPLICATION_JSON, MEDIA_TYPE_GRAPHQL}) 77 | @Produces({MediaType.APPLICATION_JSON}) 78 | @POST 79 | public void graphQLPost(@Suspended final AsyncResponse asyncResponse, 80 | @HeaderParam("Content-Type") final String contentType, 81 | String body, @Context final HttpServletRequest request) { 82 | LOGGER.debug("Invoking graphQLPost"); 83 | try { 84 | ExecutionInput executionInput = createExecutionInput(contentType, body, request); 85 | CompletionStage> mapCompletionStage = executeQuery(executionInput); 86 | mapCompletionStage.whenComplete((map, th) -> { 87 | if (th == null) { 88 | asyncResponse.resume(Response.status(200).entity(map).build()); 89 | } else { 90 | asyncResponse.resume(th); 91 | } 92 | }); 93 | 94 | } catch (Exception e) { 95 | asyncResponse.resume(e); 96 | } 97 | } 98 | 99 | /** 100 | * Executes GraphQL query using graphql-java library executeAsync option 101 | * 102 | * @param executionInput Execution Input 103 | */ 104 | private CompletionStage> executeQuery(final ExecutionInput executionInput) throws GraphqlErrorException { 105 | return graphQL.executeAsync(executionInput).thenApply((ExecutionResult executionResult) -> { 106 | LOGGER.debug("Received executionResult"); 107 | Map extensions = new HashMap<>(); 108 | executionResult = new ExecutionResultImpl(executionResult.getData(), executionResult.getErrors(), 109 | extensions); 110 | List errors = executionResult.getErrors(); 111 | errors.forEach(it -> LOGGER.error(it.toString())); 112 | if (errors != null && !errors.isEmpty()) { 113 | // GraphQL finished processing, but errors were returned in the 114 | // execution result. Resume response with a 115 | // GraphQLErrorException so it will be handled by the PPaaS 116 | // GraphQL error exception mapper. 117 | throw new MoccaServerGraphQLException(executionResult); 118 | } else { 119 | return executionResult.toSpecification(); 120 | } 121 | }); 122 | } 123 | 124 | /** 125 | * Return a {@link CompletableFuture} that completes exceptionally with the 126 | * given exception 127 | * 128 | * @param e Exception 129 | * @return {@link CompletableFuture} that completes exceptionally with the given 130 | * exception 131 | */ 132 | private CompletionStage> toCompleteableFuture(Exception e) { 133 | CompletableFuture> cf = new CompletableFuture<>(); 134 | cf.completeExceptionally(e); 135 | return cf; 136 | } 137 | 138 | /** 139 | * Create execution input based on mime type 140 | * 141 | * @param contentType request body content type 142 | * @param body request body 143 | * @param request http request 144 | * @return graphql tools execution input 145 | * @throws IOException if request cannot be serviced. 146 | */ 147 | private ExecutionInput createExecutionInput(final String contentType, final String body, HttpServletRequest request) 148 | throws IOException { 149 | 150 | if (StringUtils.isBlank(contentType)) { 151 | throw new NotSupportedException("Content-Type header is required"); 152 | } 153 | 154 | ExecutionInput executionInput; 155 | final MimeType mimeType = getMimeType(contentType); 156 | switch (mimeType.getBaseType()) { 157 | case MediaType.APPLICATION_JSON: 158 | GraphQLRequestBody graphQLRequestBody = extractGraphQLRequestBody(body); 159 | executionInput = ExecutionInput.newExecutionInput().context(getExecutionContext(request)) 160 | .query(graphQLRequestBody.getQuery()).operationName(graphQLRequestBody.getOperationName()) 161 | .variables(graphQLRequestBody.getVariables()).build(); 162 | break; 163 | case MEDIA_TYPE_GRAPHQL: 164 | executionInput = ExecutionInput.newExecutionInput().context(getExecutionContext(request)).query(body) 165 | .build(); 166 | break; 167 | default: 168 | throw new NotSupportedException(String.format("Content type %s not accepted", contentType)); 169 | } 170 | return executionInput; 171 | } 172 | 173 | /** 174 | * This creates the execution context for an {@link ExecutionInput}. An 175 | * AuthUtils will only be instantiated and added if the security context header 176 | * exists, otherwise we will not create the authUtils instance because 177 | * presumably there is no need for security context. 178 | * 179 | * @param request The {@link HttpServletRequest} from JAX-RS 180 | * @return a {@link Map} representing the execution context 181 | * @throws IOException 182 | */ 183 | private Map getExecutionContext(HttpServletRequest request) { 184 | final Map contextMap = new HashMap<>(); 185 | contextMap.put(HttpServletRequest.class.getName(), request); 186 | return contextMap; 187 | } 188 | 189 | /** 190 | * Determine if given request body contains a batched GraphQL query 191 | * 192 | * @param body Request body 193 | * @return True if batched query, false otherwise 194 | */ 195 | private boolean isBatchedQuery(String body) { 196 | try { 197 | // Attempt to read as a list of queries 198 | OBJECT_MAPPER.readValue(body, new TypeReference>() { 199 | }); 200 | // Batched query detected 201 | return true; 202 | } catch (Exception e) { 203 | // Batched query not detected 204 | return false; 205 | } 206 | } 207 | 208 | /** 209 | * Gets mime-type for a given Content-Type HTTP header. Gets it from cache if 210 | * present. 211 | * 212 | * @param contentType Content-Type HTTP header 213 | * @return MimeType 214 | * @throws NotSupportedException if mime type could not be parsed from header 215 | */ 216 | private MimeType getMimeType(final String contentType) throws NotSupportedException { 217 | return mimeTypeMap.computeIfAbsent(contentType, (key) -> { 218 | MimeType mimeType; 219 | try { 220 | /* parsing of string happens on construction */ 221 | mimeType = new MimeType(contentType); 222 | } catch (MimeTypeParseException e) { 223 | throw new NotSupportedException(String.format("Content type %s is not a valid mime type", contentType), 224 | e); 225 | } 226 | return mimeType; 227 | }); 228 | } 229 | 230 | /** 231 | * Extract graphql request body 232 | * 233 | * @param body graphql request 234 | * @return GraphQLRequestBody 235 | * @throws JsonParseException if json is malformed 236 | * @throws JsonMappingException if json cannot be mapped 237 | */ 238 | private GraphQLRequestBody extractGraphQLRequestBody(final String body) 239 | throws JsonParseException, JsonMappingException { 240 | 241 | GraphQLRequestBody graphQLRequestBody; 242 | try { 243 | graphQLRequestBody = OBJECT_MAPPER.readValue(body, GraphQLRequestBody.class); 244 | } catch (Exception e) { 245 | // Could not read body as a single query. 246 | if (isBatchedQuery(body)) { 247 | throw new RuntimeException("Batch queries not supported"); 248 | } 249 | 250 | try { 251 | // Rethrow and handle original exception with catches (sonar does 252 | // not like it when `if instanceof` statements are used) 253 | throw e; 254 | } catch (JsonParseException | JsonMappingException je) { 255 | // Rethrow to corresponding ExceptionMapper 256 | throw je; 257 | } catch (Exception fe) { 258 | throw new RuntimeException("Format not supported", fe); 259 | } 260 | 261 | } 262 | 263 | if (StringUtils.isBlank(graphQLRequestBody.getQuery())) { 264 | // Request body does not contain a query 265 | throw new RuntimeException("No Query"); 266 | } 267 | if (null == graphQLRequestBody.getVariables()) { 268 | // graphql-java now requires variables element 269 | graphQLRequestBody.setVariables(new HashMap()); 270 | } 271 | return graphQLRequestBody; 272 | 273 | } 274 | 275 | /** 276 | * Custom exception if graphql request fails. 277 | */ 278 | public static class MoccaServerGraphQLException extends RuntimeException { 279 | /** 280 | * Error message 281 | */ 282 | private static final String MESSAGE = "GraphQL errors to be handled by Exception mapper"; 283 | /** 284 | * Execution request 285 | */ 286 | private final ExecutionResult executionResult; 287 | 288 | /** 289 | * Basic constructor 290 | * 291 | * @param executionResult execution result 292 | */ 293 | public MoccaServerGraphQLException(ExecutionResult executionResult) { 294 | super(MESSAGE); 295 | this.executionResult = executionResult; 296 | } 297 | 298 | /** 299 | * Get execution result if needed 300 | * 301 | * @return ExecutionResult 302 | */ 303 | public ExecutionResult getExecutionResult() { 304 | return this.executionResult; 305 | } 306 | } 307 | 308 | } 309 | --------------------------------------------------------------------------------